Skip to content

Hybrid GDN (Qwen3.5-class) at high concurrency: mixed prefill+decode iterations run the decode portion eagerly (~4x attention cost, ~42ms/chunk overhead) — can decode stay graphed? #16976

Open

Description

@Navjot10

⚠️ Revised 2026-07-29. Our original report claimed the GPU was only ~35% busy. That was a
measurement artifact: our kernel-time sum excluded CUDA-graph replays (they appear as CUPTI
GRAPH_TRACE rows, not kernels). Correct accounting shows GPU ~86.5% busy, and the bottleneck
analysis below has been rewritten accordingly. The original text is preserved in the edit history;
the API-level data (launch/sync counts) from the original report remains valid.

Question

We serve a Qwen3.5-0.8B-architecture hybrid model (24 layers = 18 gated-deltanet + 6 full-attention, bf16 weights, fp8 KV) with trtllm-serve (PyTorch backend, 1× H100 SXM, TP1) at concurrency 384 (~19K prompt / ~3.1K decode tokens, greedy). After correcting our GPU accounting, throughput appears bounded by removable GPU work in mixed (prefill+decode) iterations rather than by host overhead. Is there a recommended path — existing or planned — to avoid running the decode portion of a mixed batch eagerly?

Setup

  • TensorRT-LLM v1.3.0rc20 and v1.3.0rc22 (both measured; +2% end-to-end difference, same profile shape)
  • max_batch_size=384, max_num_tokens tested at 32768 and 8192 (no throughput difference), max_seq_len=65536, chunked prefill, overlap scheduler, cuda_graph_config: {enable_padding: true, max_batch_size: 384}, fp8 KV, block reuse off

Corrected measurements (Nsight, graph-trace-aware; 15s steady-state, 396 iterations, 26.4 iter/s)

Global GPU busy = 86.5% of wall (kernel intervals ∪ GRAPH_TRACE intervals; nvidia-smi agrees at 85%). Graphed decode replays run ~22.2 ms each on-GPU.

Mixed (ctx) iterations: 118 of 396 = 46.8% of wall, mean 59.5 ms at max_num_tokens=8192, 86% genuine GPU time inside the range. Host Python (~25 ms/iter incl. update_requests ~9.4 ms) exists but is almost fully hidden behind the busy GPU — we estimate ≤ ~6–14% total upside from all host-side fixes combined.

The interesting structure is inside the mixed iteration cost, measured across chunk sizes:

ctx tokens in chunk mixed iteration duration
212 43 ms
1546 45 ms
3319 48 ms
5815 55 ms
7808 68–74 ms

i.e. slope ≈ 3.7 µs/token with a ~42 ms intercept that does not scale with prefill size. The intercept is the cost of dragging the 383 generation requests through the eager mixed forward:

  • generation attention (kernel_mha) inside the eager mixed pass: 13.7 ms vs ~3.4 ms for the equivalent work in the decode graph (~4×)
  • GDN v/k state fp32 cast round-trips ≈ 14% of ctx-iteration GPU time (pure dtype-conversion overhead in the hybrid path)

At max_num_tokens=32768 the same request costs one ~178 ms mixed iteration (≈ 3 × 59.5), so chunk-size tuning cannot help — the eager-decode drag is paid per chunk.

Consistent end-to-end evidence: four materially different configs (with/without one-model MTP, and a checkpoint variant with half the full-attention layers) all converge at ~3.8 req/s — decode-side changes don't move the ceiling; the mixed-iteration structure does.

Questions

  1. Mixed-iteration split: is it feasible/planned to execute the generation portion of a mixed scheduler iteration via the captured decode CUDA graph while running the context chunk in a separate (eager) forward, merging KV/scheduler state? Back-of-envelope this is worth +30–45% for small hybrid models at high concurrency. We're happy to prototype with guidance on where the invariants live.
  2. GDN fp32 casts: can the hybrid GDN state path avoid the fp32 round-trips (≈14% of ctx GPU time), or is that numerically load-bearing?
  3. Any existing configuration we've missed that reduces the eager-decode drag in mixed batches? (torch.compile piecewise CUDA graphs measured −10% on this model on rc22, both fullgraph variants.)
  4. Lower priority (mostly hidden behind the GPU, listed for completeness): batched update_requests bindings; extending [TRTLLM-14054][perf] Reduce per-step host preparation overhead in the PyTorch executor decode path #16313's steady-state fast path to one-model MTP.

We can share all .nsys-rep captures (including sync backtraces and the graph-trace-aware analysis scripts) and can test prototype branches on our rig within a day. We've also verified a small fix in this area (redundant host-syncing boolean-index in gdn_mixer state zeroing, computed 42×/step; dedup = +2% end-to-end) that we're happy to submit as a PR.

Activity

  1. added
    Inference runtime<NV>General operational aspects of TRTLLM execution not in other categories.
    Pytorch<NV>Pytorch backend related issues
    General perf<NV>Broad performance issues not specific to a particular component
    on Jul 29, 2026
  2. Navjot10 commented on Jul 29, 2026

    @Navjot10
    ContributorAuthor

    Follow-up with call-site attribution (nsys --sample=cpu --cudabacktrace=sync, same workload, v1.3.0rc22 + one-model MTP):

    The blocking cudaEventSynchronize calls (~5 per scheduler iteration, up to ~6.8 ms avg) resolve through THCPEvent_synchronize (Python torch.cuda.Event.synchronize()), and the per-iteration Python call sites are:

    • pyexecutor/py_executor.py:2719 — previous_batch.sample_state.sampler_event.synchronize() (overlap-loop wait on previous step's sampled tokens)
    • speculative/spec_sampler_base.py:193 — state.sampler_event.synchronize()
    • pyexecutor/sampler/sampler.py:171-173
    • pyexecutor/model_engine.py:6386 — _prepare_inputs_event.synchronize()
    • pyexecutor/perf_metrics_manager.py:195-197 (only when enable_iter_perf_stats: true; we measured disabling it and it does not materially change GPU-busy, so it is not the limiter)

    Observations:

    1. The steady-state fast path from [TRTLLM-14054][perf] Reduce per-step host preparation overhead in the PyTorch executor decode path #16313 explicitly excludes spec-decode batches. With a small model (0.8B-class) the per-step GPU time is only ~2–4 ms, so the sampler rendezvous + prepare path dominates wall-clock at every batch size — which is why we see 33–38% GPU busy where [TRTLLM-14054][perf] Reduce per-step host preparation overhead in the PyTorch executor decode path #16313's 35B case saw ~91%.
    2. Extending the [TRTLLM-14054][perf] Reduce per-step host preparation overhead in the PyTorch executor decode path #16313 approach (device-buffer token gather + cached batch-invariant state) to one-model MTP would need to handle variable per-sequence advancement (accepted draft counts), but seems like the highest-leverage fix for small hybrid models. Is that on the roadmap? We are happy to test any prototype on our workload and share nsys captures.
  3. Navjot10 commented on Jul 29, 2026

    @Navjot10
    ContributorAuthor

    Correction + rewrite: we've revised the issue body after re-analyzing our captures. The original "GPU only ~35% busy" claim was an accounting artifact — our kernel-time sum excluded CUDA-graph replays (GRAPH_TRACE rows). Correct accounting shows ~86.5% GPU busy, which reframes the problem from "host-bound / idle GPU" to removable GPU work in mixed prefill+decode iterations: a ~42 ms per-chunk cost that doesn't scale with prefill size, dominated by the 383 generation requests being executed eagerly (4× slower attention than the decode graph) plus fp32 cast round-trips in the GDN state path (~14% of ctx GPU time). Details, chunk-size sweep, and revised questions are in the updated body. Apologies for the churn — the API-level data in the earlier comments (launch/sync counts and call sites) remains valid.

  4. changed the title [-]Hybrid GDN (Qwen3.5-class) serving appears host/sync-bound at high concurrency — GPU only ~35% busy; expected?[/-] [+]Hybrid GDN (Qwen3.5-class) at high concurrency: mixed prefill+decode iterations run the decode portion eagerly (~4x attention cost, ~42ms/chunk overhead) — can decode stay graphed?[/+] on Jul 29, 2026
  5. mikeiovine commented on Aug 4, 2026

    @mikeiovine
    Collaborator

    Hi @Navjot10, there is currently no plan to implement full graphs for mixed batches. I suggest using the piecewise CUDA graphs for mixed batches instead:

    cuda_graph_config:
      enable_padding: true
      max_batch_size: 1024   # generation-only CUDA graphs
    torch_compile_config:
      enable_piecewise_cuda_graph: true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    General perf<NV>Broad performance issues not specific to a particular componentInference runtime<NV>General operational aspects of TRTLLM execution not in other categories.Pytorch<NV>Pytorch backend related issues

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions