Skip to content

Bind one DFlash2 draft embedding layout on every Qwen Flash rank - #250

Merged
lukealonso merged 1 commit into
mainfrom
fix/qwen-dflash2-tp-embedding
Nov 11, 2026
Merged

lukealonso merged 1 commit into
mainfrom
fix/qwen-dflash2-tp-embedding

Conversation

@original-el8

Copy link
Copy Markdown
Collaborator

Why

Tensor-parallel export of Qwen3.8 Flash Next with --speculative-method dflash2 cannot complete. Tensor-parallel ranks compile each forward once and every rank binds the same compiled artifact, and the compiled-forward bindings compare each input's shape, strides, storage offset, dtype, device and alias. Every Qwen rank holds the full replicated target embedding table (248320 × 2560 BF16), and the DFlash2 draft received a column view of that table (embed_tokens.weight[:, first:last]) for its sharded lookup. That view starts at element offset 1280 on TP2 rank 1, and at 640, 1280 and 1920 on TP4 ranks 1–3. Rank 1 therefore rejects the speculative32x4 forward with BindingMismatch during "Load and warm compiled forwards", and the export stops. Commit d68ed8f, which added Qwen Flash Next DFlash2, was GPU-validated at TP1 only.

A contiguous per-rank copy of the draft's embedding columns starts at offset zero on every rank. Other recipes that use the shared sharded-embedding lookup already pass it embedding columns loaded as separate per-rank tensors, which have that layout.

What changed

  • ignition/recipes/qwen38/dflash.py: SpeculativePreparation previously sliced the target embedding itself. It now takes a draft_embedding argument, and the new draft_embedding() function returns the rank's columns as a contiguous tensor. With one rank the column range covers the whole contiguous table, so draft_embedding() returns the target table without copying.
  • ignition/recipes/qwen38/bootstrap.py: creates the draft embedding next to the draft-head conversion, inside the core preparation pool, and passes it to SpeculativePreparation.
  • tests/recipes/test_qwen38_dflash_embedding.py: covers the per-rank layout and the shared binding signature.

TP1 DFlash2 and MTP preparation are unchanged. Each tensor-parallel rank holds one extra copy of its embedding columns: 0.64 GB per rank at TP2 and 0.32 GB at TP4, in addition to the replicated 1.27 GB target table.

Result

TP2 and TP4 DFlash2 exports of Qwen3.8 Flash Next complete on DGX Spark, and the TP2 core serves and generates. This is a correctness fix. No throughput or acceptance claim is made.

Evidence

  • Unit test: at TP1, TP2 and TP4 the test checks that every rank's draft embedding holds that rank's columns at storage offset zero. It also checks that all ranks produce one compiled-input signature (ignition.preparation.aot._signature), and that TP1 reuses the target storage. The previous column view produces two distinct signatures at TP2 and four at TP4.
  • CPU recipe tests: the Qwen and DFlash2 recipe tests have the same 24 failures with and without this change. Unmodified main has 478 passing; this branch has 482, the extra four being the added tests.
  • GPU exports: all on DGX Spark (GB10, SM121), exporting nvidia/Qwen3.8-Flash-Next-FP4-Local at recipe defaults with the incoai/Qwen3.8-Flash-Next-DFlash2 draft at revision a22a1744:
    • TP2 on two Sparks at 8c2e782d, before this change: failed in "Load and warm compiled forwards" with BindingMismatch on speculative32x4 input 59, a (248320, 1280) BF16 view expected at storage offset 0 and supplied at offset 1280.
    • TP2 with this change: warmed 76 of 76 compiled forwards, captured 112 graphs, and committed its core.
    • TP4 on four Sparks with this change: warmed 76 of 76 compiled forwards, captured 112 graphs, and committed its core.
  • Serving: the TP2 core served at concurrency 32 and answered a chat request correctly, accepting 33 draft tokens over 15 draft rounds.

Limitations

  • Serving the TP2 core needed a runtime that also includes the RoCE proxy ABI 11 change from Route multi-host RoCE rails over direct cables #232. Current main exports multi-host cores that record proxy ABI 11, but its Helios loads only cores recording ABI 9 or 10. That mismatch is independent of this change and affects every multi-host RoCE core exported from main.
  • The TP4 core has not been served.
  • DFlash2 throughput and acceptance have not been compared with MTP.

Tensor-parallel export of Qwen3.8 Flash Next with DFlash2 speculation cannot
complete. Tensor-parallel ranks compile each forward once and every rank binds
the same compiled artifact; the compiled-forward bindings compare each input's
shape, strides, storage offset, dtype, device and alias. Every rank holds the
full replicated target embedding table (248320 x 2560 BF16), and the DFlash2
draft received a column view of that table for its sharded lookup. That view
starts at element offset 1280 on TP2 rank 1 (640, 1280 and 1920 on TP4 ranks
1-3), so rank 1 rejects the speculative32x4 forward with BindingMismatch during
"Load and warm compiled forwards". Giving each rank a contiguous copy of its
columns gives every rank the same layout at offset zero, the layout the shared
sharded-embedding lookup receives from recipes that load embedding columns as
separate per-rank tensors.

Previously SpeculativePreparation in ignition/recipes/qwen38/dflash.py sliced
embed_tokens.weight[:, first:last] itself. Now draft_embedding() returns the
rank's columns as a contiguous tensor; Qwen bootstrap creates it beside the
draft head conversion, inside the core preparation pool, and passes it to
SpeculativePreparation. With one rank the column range covers the contiguous
table and draft_embedding() returns the target table without copying, so TP1
DFlash2 and MTP preparation are unchanged. Each tensor-parallel rank holds an
additional copy of its embedding columns: 0.64 GB per rank at TP2 and 0.32 GB
at TP4, alongside the replicated 1.27 GB target table.

Result: TP2 and TP4 DFlash2 exports of Qwen3.8 Flash Next complete on DGX
Spark, and the TP2 core serves.

Validation: tests/recipes/test_qwen38_dflash_embedding.py checks at TP1, TP2
and TP4 that every rank's draft embedding holds that rank's columns at storage
offset zero and that all ranks produce one compiled-input signature
(ignition.preparation.aot._signature), and that TP1 reuses the target storage.
The previous column view produces two distinct signatures at TP2 and four at
TP4. The Qwen and DFlash2 CPU recipe tests show the same 24 failures with and
without this change (478 passed on unmodified main, 482 with this change
including the four added tests).

On DGX Spark (GB10, SM121), exports of nvidia/Qwen3.8-Flash-Next-FP4-Local at
recipe defaults with --speculative-method dflash2 and the
incoai/Qwen3.8-Flash-Next-DFlash2 draft at a22a1744:
- TP2 on two Sparks, before this change on 8c2e782: failed in "Load and warm
  compiled forwards" with BindingMismatch on speculative32x4 input 59, a
  (248320, 1280) BF16 view expected at storage offset 0 and supplied at 1280.
- TP2 with this change: warmed 76/76 compiled forwards, captured 112 graphs
  and committed its core.
- TP4 on four Sparks with this change: warmed 76/76 compiled forwards,
  captured 112 graphs and committed its core.
The TP2 core served at concurrency 32 and answered a chat request correctly,
accepting 33 draft tokens over 15 draft rounds. Serving it required a runtime
that also carries the RoCE proxy ABI 11 change of PR #232, because main's
Helios loads only cores recording proxy ABI 9 or 10. The TP4 core has not been
served, and DFlash2 throughput and acceptance have not been compared with MTP.

Design-Signed-Off-By: flare-02; design-review=cfcf
@lukealonso
lukealonso merged commit 6bd20bc into main Nov 11, 2026
1 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants