feat: DFlash support for tensor-parallel Qwen3.6 targets#533
Conversation
There was a problem hiding this comment.
All reported issues were addressed across 24 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
2d32fb7 to
9279619
Compare
9279619 to
c8b3fbc
Compare
|
fyi: I have been using this for more than a day, likely used around 20-50M tokens already. So far its been good, saw a parser bug but not related to this PR.
This not too far from my 5090, but the 2x 3090 being 48gb has 2x the context size. |
c8b3fbc to
a3590be
Compare
| std::fprintf(stderr, "[backend_factory] detected arch=%s\n", arch.c_str()); | ||
|
|
||
| if (args.ds4_prefill_mode_set && arch != "deepseek4") { | ||
| if (args.device.is_tensor_parallel() && arch != "qwen35") { |
There was a problem hiding this comment.
shall we check device number here as well?
There was a problem hiding this comment.
Backend factory device validation: tensor mode now validates its
placement at the factory boundary, including the minimum device count,
duplicate devices, and incompatible split weights. Runtime-visible CUDA
device IDs and the 16-device meta-backend limit remain validated in
Qwen35TensorParallelContext::create, where the CUDA registry is available.
| // Keep the configurable upstream policy for single-device and layer-split | ||
| // paths, and lower only the effective TP threshold. | ||
| ChainRollbackPolicy rollback_policy = resolve_chain_rollback_policy(); | ||
| if (cfg_.device.is_tensor_parallel()) { |
There was a problem hiding this comment.
why this logic is not inside resolve_chain_rollback_policy?
There was a problem hiding this comment.
Rollback policy: moved the TP threshold selection into
resolve_chain_rollback_policy(bool tensor_parallel). The backend now only
passes its placement mode. Tests cover TP with both default and F32
checkpoint policies.
| } | ||
|
|
||
| bool set_device_for_tensor(const ggml_tensor * tensor) { | ||
| if (!tensor || !tensor->data) return false; |
There was a problem hiding this comment.
what is the case tensor is NULL here? I worry this check hides the bugs.
There was a problem hiding this comment.
Null tensor checks: tensor, buffer, data, and rank-local tensor
preconditions are now GGML_ASSERTs. These are internal invariants rather
than recoverable runtime failures. The helpers still return false for
actual CUDA API failures.
| namespace { | ||
|
|
||
| bool is_meta_tensor(const ggml_tensor * tensor) { | ||
| return tensor && tensor->buffer && |
There was a problem hiding this comment.
what is the case tensor is NULL here? I worry this check hides the bugs.
There was a problem hiding this comment.
Null tensor checks: tensor, buffer, data, and rank-local tensor
preconditions are now GGML_ASSERTs. These are internal invariants rather
than recoverable runtime failures. The helpers still return false for
actual CUDA API failures.
| ? src->ne[0] * src->ne[1] * src->ne[2] | ||
| : 0; | ||
| if (!src || !dst || dst->type != GGML_TYPE_F32 || | ||
| capture_index < 0 || capture_index >= src->ne[3] || |
There was a problem hiding this comment.
capture_index can be verified out of the loop. please double check so much protective checks. some checks looks like ASSERT to me.
There was a problem hiding this comment.
Repeated validation: capture_index and convolution source-slot
validation now happen once before the rank loop. Shape, type, and bounds
relationships are asserted.
| ggml_tensor * dst = ggml_backend_meta_simple_tensor(destination, rank); | ||
| if (!src || !dst || source_slots.empty() || | ||
| src->type != GGML_TYPE_F32 || | ||
| dst->type != GGML_TYPE_F32 || |
There was a problem hiding this comment.
why this have to be F32?
There was a problem hiding this comment.
Convolution F32 restriction: removed. Convolution rollback now supports
any matching non-quantized scalar type and derives offsets and widths from
the tensor element size. SSM destination remains F32 because that path
explicitly converts captured F16/F32 state into the live F32 recurrent
state.
| @@ -0,0 +1,31 @@ | |||
| #include "ggml-backend.h" | |||
There was a problem hiding this comment.
what this test really tested? seems not related to the changed code of ggml in this PR?
There was a problem hiding this comment.
CUDA communicator test: this test is inherited from stacked PR #532. It
calls ggml_backend_cuda_allreduce_tensor to verify the public compatibility
symbol still links, then passes the same CUDA backend twice to verify
duplicate ranks are rejected before NCCL communicator creation. The test now
documents both purposes inline.
|
@howard0su Thank you very much for reviewing this PR. This code base is definitely not something I can fully understand, I really appreciate your time in looking over it. All of the code is written by codex, I direct it and ask questions. I then test is on my own workload which hopefully can surface some major issues. Over the weekend 3 lucebox (Qwen 27B) was working and did all of the coding workload. |

Extend Qwen3.6 tensor parallelism from target-only autoregressive generation
to DFlash speculative decoding, including the device-local state and transfer
optimizations previously planned as a separate follow-up.
The draft remains on its explicit local device while target verification runs
through the TP meta backend. The change gathers complete target features for
the draft and extends rollback, recurrent state, snapshots, and prefix-cache
restore to split tensors.
Changes
tensors instead of gathering it through host memory.
rank after rollback.
execution.
configurable rollback policy for single-device and layer-split execution.
get_2dand oneset_2dpernon-wrapping run. Long prefills and prefix restores no longer perform two
host/device transfers and stream synchronizations per token.
DFLASH_TP_PROFILE=1.The stacked target branch also contains the review fixes for invalid device
counts, repeated split-state propagation and copies, multi-segment debug
output, legacy callback compatibility, and NaN-safe fallback all-reduce.
Verification
main@f258c4a.tool_calls.26,758-token OpenClaw request. The repeated request's prefill drops from
21.4 s to 0.1 s and produces byte-identical output.
SHA-256
3614d727a950d5416a42b5e0e3713054e59ddc9a0365fb4ac1a2cbd198f5964f,matching the saved pre-optimization and target-only continuation.
plus draft peaked at 15,208/12,640 MiB for Q5 and 20,742/18,176 MiB for Q6
on the two 3090s.
Sustained performance
Hardware: RTX 5090 plus an NVLink-connected RTX 3090 pair at stock power.
Workload: 26,758 prompt tokens, 1,024 generated tokens, 98,304 context, Q8 KV,
Q8 draft, DDTree budget 16, no prefix cache, and a 60-second cooldown between
rows. These are single runs on the final rebased head. Every row uses 98,304
context except the explicitly marked Q6/5090 capacity row.
Q5 performs 192 target forwards for 192 DDTree steps; Q6 performs 190 for
190. The optimized Q5 NVLink phase profile is:
Q5 TP is 3.01x faster than Q5 layer split and Q6 TP is 2.78x faster than Q6
layer split. Direct-P2P and P2P-disabled TP runs for both quants produced the
same byte-identical output hash shown above.
Q5 on one 24 GB 3090 failed while allocating a 1,411 MiB rollback-cache buffer
after reaching 23,286 MiB. The server logged
prefill_failed, but the HTTPlayer returned 200 with empty assistant content and
finish_reason=stop; thaterror-mapping behavior is separate from this TP PR. Q6 on the 5090 aborts with
CUDA OOM at both 98,304 and 65,536 context, but succeeds at 32,768 context with
a 31,569 MiB peak. That constrained run produced the same assistant-content
hash as Q5 on the 5090:
ac5279693a48ecc1b5e19daeadda5872f03bb47e40d589c6a40524db0596ef34.HIP was not compiled on this NVIDIA host because ROCm is not installed. The
new pointer-query calls use the existing CUDA/HIP compatibility header.