-
Notifications
You must be signed in to change notification settings - Fork 105
feat(qwen3): add DFlash2 Phase 1 path selector #1027
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
RicardoMin
wants to merge
4
commits into
pegainfer-project:main
Choose a base branch
from
RicardoMin:feat/qwen3-dflash2-phase1-930
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 1 commit
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
29ab0be
feat(qwen3): add DFlash2 Phase 1 path selector
RicardoMin 9ce2357
fix(qwen3): support native DFlash2 head variants
RicardoMin 7d04ca5
merge(qwen3): sync upstream main into DFlash2 Phase 1
RicardoMin 65e3a8c
fix(qwen3): satisfy CUDA Clippy gate
RicardoMin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| # DFlash2 Phase 1 Selector | ||
|
|
||
| > **TL;DR:** Issue #930 Phase 1 adds a bounded, deterministic top-16 candidate selector on top of the existing Qwen3 DFlash backbone; dynamic convolution, sliding-window execution, and sampled rejection are deliberately out of scope. | ||
| > | ||
| > **Last touched:** 2026-09 | ||
|
|
||
| ## Preparation | ||
|
|
||
| - **Read**: | ||
| - `docs/index.md` - routes Qwen3 model and kernel design records. | ||
| - `docs/models/qwen3/dflash-speculative-decoding.md` - defines the existing proposer/verify/KV transaction contract and the batch layout. | ||
| - `docs/models/qwen3/dspark-integration.md` - documents the legacy anchor-first/Markov path that must remain unchanged. | ||
| - `docs/models/qwen3/kernels-crate.md` - assigns CUDA primitives and FFI ownership to `pegainfer-kernels`. | ||
| - `docs/models/qwen3/model-crate.md` - documents Qwen3 model-crate boundaries and single-GPU speculative decoding. | ||
| - `docs/conventions/coding-style.md` - requires focused tests and project logging conventions. | ||
| - `CLAUDE.md` - defines build, branch, and AI-assisted contribution requirements. | ||
| - **Relevant history**: | ||
| - `docs/models/qwen3/dflash-speculative-decoding.md` - the existing DFlash lane owns proposal while the shared verify and KV transaction contracts stay method-agnostic. | ||
| - No prior DFlash2 Phase 1 task record exists in this checkout. | ||
| - **Plan**: | ||
| 1. Audit the current configuration and loader scaffold; keep legacy DFlash and DSpark behavior unchanged and reject native hybrid DFlash2 capabilities that Phase 1 cannot execute. | ||
| 2. Load and validate the selector projection/codebooks, add a fixed-size GPU selector primitive and Rust wrapper, and account for its persistent and scratch allocations. | ||
| 3. Dispatch `TopKSelector` from the DFlash draft lane without changing draft span, verify, KV transaction, or CUDA-Graph shapes. | ||
| 4. Run formatting, compile, focused selector/reference checks, GPU-vs-reference checks, and legacy DFlash/DSpark regression checks; record actual results and limitations. | ||
| - **Risks / open questions**: | ||
| - The only discovered DFlash2 checkpoint also declares Phase 2 convolution and sliding-window capabilities; it must fail closed until those execution paths exist. | ||
| - Selector tie-breaking, anchor mapping, request-major row offsets, and scratch reservation must be deterministic and shape-safe. | ||
|
|
||
| ## Execution Log | ||
|
|
||
| ### Step 1: Normalize the DFlash2 capability contract | ||
|
|
||
| - Added a `DFlashProposal::TopKSelector` capability and an explicit | ||
| `DFlashLayout` in `pegainfer-qwen3/src/config.rs`. | ||
| - Legacy DFlash and DSpark schemas remain on their existing proposal paths. | ||
| - Native DFlash2 configurations are parsed, but Phase 2 convolution, | ||
| sliding-window attention, anchor-first selector layouts, and an untied draft | ||
| output head fail closed before GPU weight allocation. | ||
|
|
||
| ### Step 2: Load selector weights and wire the proposer | ||
|
|
||
| - Added SafeTensors manifest checks for the hidden projection and predecessor / | ||
| successor codebooks. | ||
| - Added persistent selector scratch and a two-launch CUDA implementation: | ||
| deterministic top-16 candidate extraction followed by a request-local path | ||
| walk using the predecessor/successor codebooks. | ||
| - Kept the existing full-block draft result contract, verify span, KV updates, | ||
| and CUDA-Graph shapes unchanged. | ||
|
|
||
| ### Step 3: Fix anchor-drop row mapping | ||
|
|
||
| - The DFlash backbone emits an anchor-inclusive block. For the current | ||
| anchor-drop layout, row 0 is discarded by the executor and rows 1..N-1 are | ||
| the real proposal positions. | ||
| - The selector now uses compact candidate/output rows for those real positions, | ||
| while reading the corresponding rows from the original anchor-inclusive | ||
| logits/hidden buffers. Every request-local walk starts from the verified | ||
| anchor token, so no draft depends on a candidate from the discarded row 0. | ||
| - The host wrapper reconstructs `[anchor, selected_1, ..., selected_N-1]` for | ||
| the unchanged executor contract and rejects an invalid GPU token id before | ||
| it can reach token lookup. | ||
|
|
||
| ### Step 4: Lightweight verification | ||
|
|
||
| Commands were run in the Linux feature checkout | ||
| `/database/ricardo.zheng/projects/open-access/pegainfer` | ||
| with `/usr/bin` present in `PATH` (the build script invokes `git`): | ||
|
|
||
| | Command | Result | | ||
| | --- | --- | | ||
| | `cargo fmt --all -- --check` | Passed | | ||
| | `git diff --check` | Passed | | ||
| | `cargo check --release -p pegainfer-qwen3 --tests` | Passed; CUDA `sm_89` build | | ||
| | `cargo test --release -p pegainfer-qwen3 --lib` | 88 passed, 0 failed | | ||
| | `cargo test --release -p pegainfer-build --lib` | 8 passed, 0 failed | | ||
|
|
||
| ### Step 5: Remove redundant scaffolding | ||
|
|
||
| - Kept the selector tensor preflight because the shared loader does not check | ||
| SafeTensors dtype or malformed rank; removed its unused `SelectorManifest` | ||
| wrapper and duplicate positive-value checks. | ||
| - Removed the unused selector scratch accessor and ABI-only bf16 assertion. | ||
| - Shortened comments to the anchor mapping, two-launch dependency, and | ||
| unsupported-capability boundaries. | ||
| - Re-ran formatting, Qwen3/build tests, and the server release build. | ||
|
|
||
| | Cleanup verification | Result | | ||
| | --- | --- | | ||
| | `cargo fmt --all -- --check` | Passed | | ||
| | `git diff --check` | Passed | | ||
| | `cargo check --release -p pegainfer-qwen3 --tests` | Passed | | ||
| | `cargo test --release -p pegainfer-qwen3 --lib` | 88 passed, 0 failed | | ||
| | `cargo test --release -p pegainfer-build --lib` | 8 passed, 0 failed | | ||
| | `cargo build --release -p pegainfer-server --bin pegainfer` | Passed | | ||
|
|
||
| ## Debrief | ||
|
|
||
| - **Outcome:** Phase 1 selector wiring, anchor-drop mapping, and a focused | ||
| cleanup of redundant scaffolding are complete in the feature branch. The | ||
| checkout is fast-forwarded to upstream main; no changes are staged or | ||
| committed. | ||
| - **Pitfalls encountered:** The first verification command omitted system | ||
| directories from `PATH`, so `pegainfer-kernels/build.rs` could not spawn | ||
| `git`. Re-running with `/usr/bin:/bin` succeeded. The row mapping bug was a | ||
| real semantic issue that compilation alone could not detect. | ||
| - **Lessons learned:** Selector buffers must distinguish the input block shape | ||
| from the compact set of positions actually proposed. The anchor is a | ||
| request-level predecessor, not a selector candidate when the executor drops | ||
| row 0. | ||
| - **Follow-ups:** Run the GPU selector/checkpoint import, Qwen3 greedy | ||
| losslessness, legacy DFlash/DSpark regressions, HTTP serving, and performance | ||
| A/B on the Linux GPU host. Phase 2 remains responsible for dynamic | ||
| convolution and sliding-window execution; Phase 3 remains responsible for | ||
| sampled losslessness/rejection sampling. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,222 @@ | ||
| #include "common.cuh" | ||
|
|
||
| #include <climits> | ||
| #include <cmath> | ||
| #include <cstdint> | ||
| #include <cuda.h> | ||
| #include <cuda_bf16.h> | ||
| #include <cuda_runtime.h> | ||
|
|
||
| // DFlash2 keeps a small candidate set per draft position. The selector is | ||
| // deliberately split into two kernels: top-k is embarrassingly parallel over | ||
| // logits, while the path walk has a request-local dependency on the previously | ||
| // selected token. Keeping that dependency out of the top-k kernel makes both | ||
| // launches easy to reason about and keeps the temporary layout graph-safe. | ||
| namespace { | ||
|
|
||
| constexpr int SELECTOR_TOP_K = 16; | ||
| constexpr int SELECTOR_TOPK_THREADS = 256; | ||
| constexpr int SELECTOR_WALK_THREADS = 512; | ||
|
|
||
| __device__ __forceinline__ bool selector_better(float lhs_value, int lhs_id, | ||
| float rhs_value, int rhs_id) { | ||
| return lhs_value > rhs_value || | ||
| (lhs_value == rhs_value && lhs_id < rhs_id); | ||
| } | ||
|
|
||
| __device__ __forceinline__ void selector_insert(float value, int id, float* values, int* ids) { | ||
| if (!selector_better(value, id, values[SELECTOR_TOP_K - 1], | ||
| ids[SELECTOR_TOP_K - 1])) { | ||
| return; | ||
| } | ||
| int slot = SELECTOR_TOP_K - 1; | ||
| while (slot > 0 && | ||
| selector_better(value, id, values[slot - 1], ids[slot - 1])) { | ||
| values[slot] = values[slot - 1]; | ||
| ids[slot] = ids[slot - 1]; | ||
| --slot; | ||
| } | ||
| values[slot] = value; | ||
| ids[slot] = id; | ||
| } | ||
|
|
||
| __global__ void dflash2_selector_topk_kernel( | ||
| const __nv_bfloat16* __restrict__ logits, uint32_t* __restrict__ ids, | ||
| float* __restrict__ scores, int rows, int input_block_size, | ||
| int position_offset, int positions_per_request, int vocab) { | ||
| // Output rows are compact, while the source logits retain the anchor row. | ||
| // Translate each compact row back to its request-major input row. | ||
| const int row = blockIdx.x; | ||
| if (row >= rows || positions_per_request <= 0) { | ||
| return; | ||
| } | ||
| const int request = row / positions_per_request; | ||
| const int position = row % positions_per_request; | ||
| const size_t source_row = static_cast<size_t>(request) * input_block_size + | ||
| position_offset + position; | ||
|
|
||
| // Each thread keeps a private top-16 list. The lists occupy 32 KiB of | ||
| // shared memory and are merged by thread zero in canonical score/id order. | ||
| __shared__ float local_values[SELECTOR_TOPK_THREADS][SELECTOR_TOP_K]; | ||
| __shared__ int local_ids[SELECTOR_TOPK_THREADS][SELECTOR_TOP_K]; | ||
| float* my_values = local_values[threadIdx.x]; | ||
| int* my_ids = local_ids[threadIdx.x]; | ||
| for (int j = 0; j < SELECTOR_TOP_K; ++j) { | ||
| my_values[j] = -INFINITY; | ||
| my_ids[j] = INT_MAX; | ||
| } | ||
|
|
||
| const __nv_bfloat16* row_logits = logits + source_row * vocab; | ||
| for (int token = threadIdx.x; token < vocab; | ||
| token += SELECTOR_TOPK_THREADS) { | ||
| selector_insert(__bfloat162float(row_logits[token]), token, my_values, | ||
| my_ids); | ||
| } | ||
| __syncthreads(); | ||
|
|
||
| if (threadIdx.x == 0) { | ||
| float best_values[SELECTOR_TOP_K]; | ||
| int best_ids[SELECTOR_TOP_K]; | ||
| for (int j = 0; j < SELECTOR_TOP_K; ++j) { | ||
| best_values[j] = -INFINITY; | ||
| best_ids[j] = INT_MAX; | ||
| } | ||
| for (int thread = 0; thread < SELECTOR_TOPK_THREADS; ++thread) { | ||
| for (int j = 0; j < SELECTOR_TOP_K; ++j) { | ||
| selector_insert(local_values[thread][j], local_ids[thread][j], | ||
| best_values, best_ids); | ||
| } | ||
| } | ||
| for (int j = 0; j < SELECTOR_TOP_K; ++j) { | ||
| ids[static_cast<size_t>(row) * SELECTOR_TOP_K + j] = | ||
| static_cast<uint32_t>(best_ids[j]); | ||
| scores[static_cast<size_t>(row) * SELECTOR_TOP_K + j] = best_values[j]; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| __global__ void dflash2_selector_walk_kernel( | ||
| const __nv_bfloat16* __restrict__ projected_hidden, | ||
| const __nv_bfloat16* __restrict__ predecessor, | ||
| const __nv_bfloat16* __restrict__ successor, | ||
| const uint32_t* __restrict__ anchor_tokens, | ||
| const uint32_t* __restrict__ candidate_ids, | ||
| const float* __restrict__ candidate_unary, uint32_t* __restrict__ output, | ||
| int requests, int input_block_size, int position_offset, | ||
| int positions_per_request, int vocab, int rank) { | ||
| const int request = blockIdx.x; | ||
| if (request >= requests) { | ||
| return; | ||
| } | ||
|
|
||
| __shared__ float edge_scores[SELECTOR_TOP_K]; | ||
| __shared__ uint32_t edge_ids[SELECTOR_TOP_K]; | ||
| __shared__ uint32_t previous; | ||
| if (threadIdx.x == 0) { | ||
| previous = anchor_tokens[request]; | ||
| } | ||
| __syncthreads(); | ||
|
|
||
| const int lane = threadIdx.x & 31; | ||
| const int candidate = threadIdx.x >> 5; | ||
| for (int position = 0; position < positions_per_request; ++position) { | ||
| // Candidate/output rows are compact; hidden rows retain the anchor slot. | ||
| const int row = request * positions_per_request + position; | ||
| const size_t source_row = static_cast<size_t>(request) * input_block_size + | ||
| position_offset + position; | ||
| if (candidate < SELECTOR_TOP_K) { | ||
| const uint32_t candidate_id = | ||
| candidate_ids[static_cast<size_t>(row) * SELECTOR_TOP_K + candidate]; | ||
| float dot = 0.0f; | ||
| if (candidate_id < static_cast<uint32_t>(vocab) && | ||
| previous < static_cast<uint32_t>(vocab)) { | ||
| const __nv_bfloat16* hidden_row = | ||
| projected_hidden + source_row * rank; | ||
| const __nv_bfloat16* predecessor_row = | ||
| predecessor + static_cast<size_t>(previous) * rank; | ||
| const __nv_bfloat16* successor_row = | ||
| successor + static_cast<size_t>(candidate_id) * rank; | ||
| for (int component = lane; component < rank; component += 32) { | ||
| dot += __bfloat162float(predecessor_row[component]) * | ||
| __bfloat162float(hidden_row[component]) * | ||
| __bfloat162float(successor_row[component]); | ||
| } | ||
| } | ||
| dot = warp_reduce_sum(dot); | ||
| if (lane == 0) { | ||
| edge_ids[candidate] = candidate_id; | ||
| edge_scores[candidate] = | ||
| candidate_unary[static_cast<size_t>(row) * SELECTOR_TOP_K + | ||
| candidate] + | ||
| dot; | ||
| } | ||
| } | ||
| __syncthreads(); | ||
|
|
||
| if (threadIdx.x == 0) { | ||
| uint32_t best_id = edge_ids[0]; | ||
| float best_score = edge_scores[0]; | ||
| for (int j = 1; j < SELECTOR_TOP_K; ++j) { | ||
| if (selector_better(edge_scores[j], static_cast<int>(edge_ids[j]), | ||
| best_score, static_cast<int>(best_id))) { | ||
| best_score = edge_scores[j]; | ||
| best_id = edge_ids[j]; | ||
| } | ||
| } | ||
| output[row] = best_id; | ||
| previous = best_id; | ||
| } | ||
| __syncthreads(); | ||
| } | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| extern "C" int dflash2_selector_topk_cuda( | ||
| const __nv_bfloat16* logits, uint32_t* candidate_ids, | ||
| float* candidate_scores, int rows, int input_block_size, | ||
| int position_offset, int positions_per_request, int vocab, | ||
| cudaStream_t stream) { | ||
| if (logits == nullptr || candidate_ids == nullptr || candidate_scores == nullptr || | ||
| rows <= 0 || input_block_size <= 0 || position_offset < 0 || | ||
| positions_per_request <= 0 || | ||
| position_offset > input_block_size - positions_per_request || | ||
| vocab < SELECTOR_TOP_K) { | ||
| return static_cast<int>(cudaErrorInvalidValue); | ||
| } | ||
| dflash2_selector_topk_kernel<<<rows, SELECTOR_TOPK_THREADS, 0, stream>>>( | ||
| logits, candidate_ids, candidate_scores, rows, input_block_size, | ||
| position_offset, positions_per_request, vocab); | ||
| return static_cast<int>(cudaGetLastError()); | ||
| } | ||
|
|
||
| extern "C" int dflash2_selector_walk_cuda( | ||
| const __nv_bfloat16* projected_hidden, | ||
| const __nv_bfloat16* predecessor, | ||
| const __nv_bfloat16* successor, | ||
| const uint32_t* anchor_tokens, | ||
| const uint32_t* candidate_ids, | ||
| const float* candidate_unary, | ||
| uint32_t* output, | ||
| int requests, | ||
| int input_block_size, | ||
| int position_offset, | ||
| int positions_per_request, | ||
| int vocab, | ||
| int rank, | ||
| cudaStream_t stream) { | ||
| if (projected_hidden == nullptr || predecessor == nullptr || | ||
| successor == nullptr || anchor_tokens == nullptr || candidate_ids == nullptr || | ||
| candidate_unary == nullptr || output == nullptr || requests <= 0 || | ||
| input_block_size <= 0 || position_offset < 0 || | ||
| positions_per_request <= 0 || | ||
| position_offset > input_block_size - positions_per_request || | ||
| vocab < SELECTOR_TOP_K || rank <= 0) { | ||
| return static_cast<int>(cudaErrorInvalidValue); | ||
| } | ||
| dflash2_selector_walk_kernel<<<requests, SELECTOR_WALK_THREADS, 0, stream>>>( | ||
| projected_hidden, predecessor, successor, anchor_tokens, | ||
| candidate_ids, candidate_unary, output, requests, input_block_size, | ||
| position_offset, positions_per_request, vocab, rank); | ||
| return static_cast<int>(cudaGetLastError()); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.