Summary
On 8×MI350X, Kimi-K3 DSpark improves 4k/1k TP8/EP8 throughput by 39–106%, but at 131,072-token input its average accepted length collapses to exactly 1.0, turning the extra 7 draft positions plus 8-wide target verification into overhead and making decode 28–41% slower than ordinary TokenSpeed.
The first-principles performance target is: make one DSpark verification round a first-class vectorized M=8 execution path whose cost approaches one ordinary decode round, independent of how many candidates are eventually accepted. Acceptance determines how many output tokens amortize that round, but inefficient infrastructure currently inflates the numerator by 2.39×; that underlying cost should be fixed before relying on an acceptance-based fallback.
Reproduction contract
- TokenSpeed:
8911ce4cd7bd9f4fcea33bc069a5d0f27cebfa95
- Hardware: 8×AMD Instinct MI350X (
gfx950)
- Model: Kimi-K3 DSpark checkpoint
- Topology: TP8/EP8
- Runtime: FP8 KV cache, graph-compiled decode, eager/chunked prefill, greedy sampling
- Workload: identical random-token prompts, exact ISL/OSL, prefix caching explicitly disabled
- “132K” below means ISL=131,072 and OSL=1,024 (132,096 total tokens/request)
Current end-to-end behavior
| Shape |
Non-DSpark output tok/s |
DSpark output tok/s |
Delta |
Non-DSpark TPOT |
DSpark TPOT |
DSpark / base TPOT |
| 4K/1K B1/C1 |
66.30 |
136.28 |
+105.6% |
14.62 ms |
6.83 ms |
0.47× |
| 4K/1K B8/C8 |
223.59 |
348.55 |
+55.9% |
32.97 ms |
18.74 ms |
0.57× |
| 4K/1K B16/C16 |
331.84 |
459.90 |
+38.6% |
42.76 ms |
27.64 ms |
0.65× |
| 132K/1K B1/C1 |
30.45 |
17.88 |
-41.3% |
18.04 ms |
40.69 ms |
2.26× |
| 132K/1K B2/C2 |
36.55 |
23.65 |
-35.3% |
24.96 ms |
54.03 ms |
2.16× |
| 132K/1K B4/C4 |
43.44 |
31.12 |
-28.4% |
27.29 ms |
65.57 ms |
2.40× |
The 132K B1 TTFT is nearly unchanged (15.14 s base versus 15.60 s DSpark), so this is a decode regression, not a long-prefill regression. Long B8 DSpark is omitted because its smaller KV pool admitted only part of the requested concurrency.
At 4K, the server reports accepted lengths of roughly 4–7 tokens, enough to repay the wider work. At 132K B1, the log reports:
Accept_num_tokens_avg: 1.0
First-64-decode-step kernel breakdown
The table sums rank-0 GPU kernel durations over the captured decode window. These are dispatch sums for attribution, not wall time: kernels/collectives may overlap or wait, and rank 0 may not be the critical rank.
| Component / exact kernel |
Non-DSpark calls |
Base total |
DSpark calls |
DSpark total |
DSpark/base |
DSpark share |
TP collective/residual: iris_stage_one_shot_allreduce_residual_attnres_gluon_kernel.kd (base) vs iris_stage_one_shot_allreduce_kernel.kd (DSpark) |
5,355 |
279.32 ms |
6,784 |
721.92 ms |
2.58× |
20.22% |
MoE symmetric reduction: iris_reduce_symmetric_gluon_kernel.kd |
5,888 |
220.72 ms |
5,888 |
602.30 ms |
2.73× |
16.87% |
MoE W13 + SiTU: _stage1_a16w4_situ_warp_gemv.kd |
5,888 |
76.78 ms |
5,888 |
410.79 ms |
5.35× |
11.50% |
MLA decode: _mla_decode_gluon.kd |
1,536 |
232.20 ms |
1,856 |
217.00 ms |
0.93× |
6.08% |
MoE W2/combine: _stage2_a16w4_warp_gemv_combine.kd |
5,888 |
54.88 ms |
5,888 |
191.26 ms |
3.49× |
5.36% |
MoE routing: _sigmoid_bias_topk_route_gluon_kernel.kd |
— |
outside base top 10 |
5,796 |
179.12 ms |
— |
5.02% |
Dense/projection GEMM: Cijk_...MT32x16x512...ISA950...kd |
— |
comparable base GEMM family 64.63 ms |
11,754 |
132.90 ms |
— |
3.72% |
Attention residual: _attn_res_rmsnorm_kernel.kd |
structurally fused/different base path |
— |
11,968 |
116.97 ms |
— |
3.28% |
MoE projection: _packed_projection_gemm_kernel.kd |
— |
outside base top 10 |
5,888 |
115.09 ms |
— |
3.22% |
KDA: fused_recurrent_kda_mtp_fwd_kernel.kd |
different base kernel |
— |
4,416 |
107.12 ms |
— |
3.00% |
| All rank-0 decode dispatch |
— |
1,491.90 ms |
— |
3,571.08 ms |
2.39× |
100% |
The three largest regressions are especially revealing:
TP collective/residual: +442.60 ms
MoE symmetric reduction: +381.58 ms
MoE W13 + SiTU: +334.01 ms
MoE W2/combine: +136.37 ms
Those four matched groups explain about 62% of the extra rank-0 dispatch time. MLA is not the first target: despite more calls, its total time is slightly lower in this capture.
The kernel shape, not merely launch count, is hurting. W13 and W2 have the same number of calls in both modes, but mean call time grows from 13.04 to 69.77 µs and from 9.32 to 32.48 µs respectively. The TP path also changes from a fused allreduce_residual_attnres kernel to a plain all-reduce plus separate residual work.
Separate infrastructure efficiency from acceptance
For draft width k and accepted length a, DSpark performance decomposes into two independent quantities:
speculative_round_cost
= draft_cost(k) + target_verify_cost(k) + routing/state overhead
infrastructure efficiency
= speculative_round_cost / ordinary_decode_round_cost
algorithmic efficiency
= accepted_tokens / speculative_round
DSpark cost per output token
= speculative_round_cost / accepted_tokens
The current long-context result has poor values for both dimensions: acceptance is 1.0, while rank-0 dispatch work per profiled window is 2.39× ordinary decode. We should optimize and report the round-cost ratio even under forced rejection, so good acceptance cannot hide inefficient kernels and bad acceptance cannot hide infrastructure improvements.
At acceptance 1.0, DSpark necessarily performs some extra draft/state work, so exact parity is not guaranteed. However, K3 decode is largely weight-, memory-, and synchronization-bound: an M=8 target pass should reuse fixed weights, share KV-cache tiles across query rows, and aggregate collectives rather than paying close to eight independent token costs. The current 2.39× ratio—and especially the 2.58–5.35× growth in dominant components—leaves substantial non-fundamental overhead to remove.
Proposed fix, in priority order
P0 — make the whole M=8 verification round a first-class execution path
Treat the candidate-token dimension as a small batch carried end-to-end through routing, expert computation, collectives, residuals, and state updates—not as eight expensive variations of M=1 decode:
candidate rows
│
▼
one vectorized routing/packing pass
│
▼
occupancy-aware grouped MoE with shared weight loads
│
▼
one collective carrying all candidate rows
│
▼
fused residual + state/cache update
- Profile a single speculative round and compare it directly with a single ordinary round at identical context and active request count.
- Track useful bytes versus actual HBM bytes for fixed projections, KV-cache reads, and routed experts. Fixed weights and KV tiles should be reused across candidate rows rather than reloaded per row.
- Track dispatches per round, intermediate tensor materializations, and the slowest rank's critical path.
- Preserve the candidate dimension through operators; do not repeatedly flatten, copy, repad, or re-route it between layers.
The initial milestone is ≤1.5× ordinary round wall time under forced acceptance=1; the stretch target is ≤1.2×. These are infrastructure targets, not claims that low-acceptance speculation will outperform ordinary decoding.
P1 — make M=8 collective/residual work resemble the base fused path
- Recover the producer-direct
allreduce + residual + AttentionRes fusion for the DSpark M dimension instead of using plain iris_stage_one_shot_allreduce_kernel followed by separate residual kernels.
- Measure per-rank producer-arrival and Iris wait time. If the collective duration is mostly waiting for MoE, fix the producer imbalance before tuning the reduction loop.
- Preserve vectorized M rows and avoid materializing intermediate
[M, hidden] tensors between target verification stages.
This attacks the 721.92 ms TP path and 602.30 ms MoE reduction path, but must be judged on critical-path wall time across all ranks, not rank-0 summed duration.
P2 — add an M=8-aware routed-expert schedule and reuse weights
The existing warp-GEMV path scales poorly for the verified rows: W13 grows 5.35× and W2 3.49× with unchanged call count. Bucket expert work by actual token occupancy and use small-M grouped tiles when an expert receives multiple verified rows, while retaining the fast M=1 kernel for singleton experts. Load an expert's packed weights/scales once for all of its candidate rows where occupancy allows it, and fuse SiTU and weighted-combine epilogues only where doing so shortens time-to-last-expert.
Also record selected expert IDs per layer/rank. A slow rank or a few hot experts can extend both the MoE kernel tail and the following Iris wait.
P3 — vectorize routing, projections, KV access, and state updates
- Fuse or share route preparation across the draft/verify stages where semantics allow it.
- Remove copies/layout materializations around
_packed_projection_gemm_kernel.
- Tile MLA over candidate queries so long-context KV tiles can be reused across rows rather than streamed independently where the attention semantics permit it.
- Batch KDA/recurrent-state and page-table updates across candidates, then commit only the accepted prefix without rebuilding the candidate state one token at a time.
- Tune graph buckets at the real active-row count and preserve vectorized candidate rows rather than padding or decomposing them into M=1 operations.
The current MLA total is already slightly lower in the captured DSpark window, so validate its HBM traffic before changing it; the immediate evidence points more strongly to MoE and collectives.
P4 — separately diagnose the acceptance collapse
Low acceptance can be legitimate for some workloads, but 1.0 at 131K is suspicious enough to investigate independently:
- Sweep 4K/32K/64K/96K/131K with the same prompts and record per-position acceptance histograms, not only one average.
- Compare draft and target logits/tokens at the first rejected position.
- Audit long-position IDs/rotary state, draft-layer KV offsets/page tables, recurrent KDA state injection, and cache updates after partial acceptance.
- Repeat with natural-text prompts; fixed random-token prompts may be out-of-distribution for the drafter.
- Distinguish checkpoint-quality degradation from an implementation defect before changing model semantics.
Acceptance investigation is deliberately separate from infrastructure optimization. We should be able to show that the DSpark round became cheaper even if this particular random-token workload continues to accept only one token.
P5 — add fallback only as a production guardrail
After the underlying path is efficient, an optional cost-aware controller can avoid inherently unprofitable speculation. This is not the main performance fix and must not conceal an acceptance or state-management bug.
If implemented, pre-capture ordinary and useful DSpark graph widths, compare measured round cost per accepted token, use hysteresis, and keep profitable requests separate from fallback requests when batching permits. Do not hard-code “disable after 128K”; context length is at most a cold-start prior because acceptance depends on the request and decode position.
Validation gates
- Forced-low-acceptance infrastructure test: deterministically reject every draft continuation but keep M=8 verification enabled. Report DSpark-round/ordinary-round wall time, dispatch count, HBM bytes, and slowest-rank time. First target ≤1.5×; stretch ≤1.2×, down from the current 2.39× rank-0 dispatch ratio.
- Per-component gates: reduce TP collective/residual, MoE reduction, W13, and W2 ratios from 2.58×/2.73×/5.35×/3.49× while preserving output. Report both summed dispatch and non-overlapping critical-path changes.
- Context sweep: 4K/32K/64K/96K/131K, B1, 1K output, three runs each; publish acceptance histogram, round-cost ratio, TPOT, and output tok/s as separate columns.
- Batch sweep: exact 131K/1K at B1/B2/B4. Report infrastructure ratio independently from the resulting acceptance-amortized TPOT.
- Protect the win: exact 4K/1K B1/B8/B16 must retain at least 95% of current DSpark throughput (136.28/348.55/459.90 tok/s).
- Profile correctly: unprofiled full-1K runs decide performance; separate first-64-step traces explain it. Capture every rank and report the slowest-rank critical path plus kernel dispatch sums.
- Correctness: run Kimi-K3 AIME26 and targeted draft/target logit comparisons through rejection, partial acceptance, and full acceptance.
- Capacity: do not report a requested batch when DSpark's draft-layer KV allocation queues some requests; report admitted concurrency explicitly.
Benchmark caveats and artifacts
- Non-DSpark exposes 1,897,600 cache tokens; DSpark exposes 1,106,560 because five draft layers also consume cache.
- Prefix caching was disabled. A fixed-seed control with caching reused up to 130,944 prompt tokens and is excluded.
- The current-main AMD benchmark required the same temporary vendor-neutral
mla_kv_pack_quantize_fp8 fallback in both modes and a startup-only pageable-CPU checkpoint-loading workaround; neither is inside the timed/profiled interval.
- Local raw logs, throughput JSON, 64 rank-local Perfetto traces, and parsed kernel JSON are under
/home/stwinata/k3_sgl_vs_ts_perf/artifacts/dspark_comparison_20260818/.
Related performance context: #55 (TokenSpeed TP8/EP8 vs SGLang), #57 (B1 roofline), and #58 (B8 roofline).
Summary
On 8×MI350X, Kimi-K3 DSpark improves 4k/1k TP8/EP8 throughput by 39–106%, but at 131,072-token input its average accepted length collapses to exactly
1.0, turning the extra 7 draft positions plus 8-wide target verification into overhead and making decode 28–41% slower than ordinary TokenSpeed.The first-principles performance target is: make one DSpark verification round a first-class vectorized M=8 execution path whose cost approaches one ordinary decode round, independent of how many candidates are eventually accepted. Acceptance determines how many output tokens amortize that round, but inefficient infrastructure currently inflates the numerator by 2.39×; that underlying cost should be fixed before relying on an acceptance-based fallback.
Reproduction contract
8911ce4cd7bd9f4fcea33bc069a5d0f27cebfa95gfx950)Current end-to-end behavior
The 132K B1 TTFT is nearly unchanged (15.14 s base versus 15.60 s DSpark), so this is a decode regression, not a long-prefill regression. Long B8 DSpark is omitted because its smaller KV pool admitted only part of the requested concurrency.
At 4K, the server reports accepted lengths of roughly 4–7 tokens, enough to repay the wider work. At 132K B1, the log reports:
First-64-decode-step kernel breakdown
The table sums rank-0 GPU kernel durations over the captured decode window. These are dispatch sums for attribution, not wall time: kernels/collectives may overlap or wait, and rank 0 may not be the critical rank.
iris_stage_one_shot_allreduce_residual_attnres_gluon_kernel.kd(base) vsiris_stage_one_shot_allreduce_kernel.kd(DSpark)iris_reduce_symmetric_gluon_kernel.kd_stage1_a16w4_situ_warp_gemv.kd_mla_decode_gluon.kd_stage2_a16w4_warp_gemv_combine.kd_sigmoid_bias_topk_route_gluon_kernel.kdCijk_...MT32x16x512...ISA950...kd_attn_res_rmsnorm_kernel.kd_packed_projection_gemm_kernel.kdfused_recurrent_kda_mtp_fwd_kernel.kdThe three largest regressions are especially revealing:
Those four matched groups explain about 62% of the extra rank-0 dispatch time. MLA is not the first target: despite more calls, its total time is slightly lower in this capture.
The kernel shape, not merely launch count, is hurting. W13 and W2 have the same number of calls in both modes, but mean call time grows from 13.04 to 69.77 µs and from 9.32 to 32.48 µs respectively. The TP path also changes from a fused
allreduce_residual_attnreskernel to a plain all-reduce plus separate residual work.Separate infrastructure efficiency from acceptance
For draft width
kand accepted lengtha, DSpark performance decomposes into two independent quantities:The current long-context result has poor values for both dimensions: acceptance is
1.0, while rank-0 dispatch work per profiled window is 2.39× ordinary decode. We should optimize and report the round-cost ratio even under forced rejection, so good acceptance cannot hide inefficient kernels and bad acceptance cannot hide infrastructure improvements.At acceptance
1.0, DSpark necessarily performs some extra draft/state work, so exact parity is not guaranteed. However, K3 decode is largely weight-, memory-, and synchronization-bound: an M=8 target pass should reuse fixed weights, share KV-cache tiles across query rows, and aggregate collectives rather than paying close to eight independent token costs. The current 2.39× ratio—and especially the 2.58–5.35× growth in dominant components—leaves substantial non-fundamental overhead to remove.Proposed fix, in priority order
P0 — make the whole M=8 verification round a first-class execution path
Treat the candidate-token dimension as a small batch carried end-to-end through routing, expert computation, collectives, residuals, and state updates—not as eight expensive variations of M=1 decode:
The initial milestone is ≤1.5× ordinary round wall time under forced acceptance=1; the stretch target is ≤1.2×. These are infrastructure targets, not claims that low-acceptance speculation will outperform ordinary decoding.
P1 — make M=8 collective/residual work resemble the base fused path
allreduce + residual + AttentionResfusion for the DSpark M dimension instead of using plainiris_stage_one_shot_allreduce_kernelfollowed by separate residual kernels.[M, hidden]tensors between target verification stages.This attacks the 721.92 ms TP path and 602.30 ms MoE reduction path, but must be judged on critical-path wall time across all ranks, not rank-0 summed duration.
P2 — add an M=8-aware routed-expert schedule and reuse weights
The existing warp-GEMV path scales poorly for the verified rows: W13 grows 5.35× and W2 3.49× with unchanged call count. Bucket expert work by actual token occupancy and use small-M grouped tiles when an expert receives multiple verified rows, while retaining the fast M=1 kernel for singleton experts. Load an expert's packed weights/scales once for all of its candidate rows where occupancy allows it, and fuse SiTU and weighted-combine epilogues only where doing so shortens time-to-last-expert.
Also record selected expert IDs per layer/rank. A slow rank or a few hot experts can extend both the MoE kernel tail and the following Iris wait.
P3 — vectorize routing, projections, KV access, and state updates
_packed_projection_gemm_kernel.The current MLA total is already slightly lower in the captured DSpark window, so validate its HBM traffic before changing it; the immediate evidence points more strongly to MoE and collectives.
P4 — separately diagnose the acceptance collapse
Low acceptance can be legitimate for some workloads, but
1.0at 131K is suspicious enough to investigate independently:Acceptance investigation is deliberately separate from infrastructure optimization. We should be able to show that the DSpark round became cheaper even if this particular random-token workload continues to accept only one token.
P5 — add fallback only as a production guardrail
After the underlying path is efficient, an optional cost-aware controller can avoid inherently unprofitable speculation. This is not the main performance fix and must not conceal an acceptance or state-management bug.
If implemented, pre-capture ordinary and useful DSpark graph widths, compare measured round cost per accepted token, use hysteresis, and keep profitable requests separate from fallback requests when batching permits. Do not hard-code “disable after 128K”; context length is at most a cold-start prior because acceptance depends on the request and decode position.
Validation gates
Benchmark caveats and artifacts
mla_kv_pack_quantize_fp8fallback in both modes and a startup-only pageable-CPU checkpoint-loading workaround; neither is inside the timed/profiled interval./home/stwinata/k3_sgl_vs_ts_perf/artifacts/dspark_comparison_20260818/.Related performance context: #55 (TokenSpeed TP8/EP8 vs SGLang), #57 (B1 roofline), and #58 (B8 roofline).