Found during a performance review of the v20 r12 release image voipmonitor/vllm:gilded-gnosis-v20-vllmb46c3aa-si35aebc6-fi801d57a-cu132-20260730-r12. Code refs are against the composed r12 tree; all cited sparkinfer files are byte-identical to public commit de04125508c75e748016493f851e6bd4513b515b, so permalinks use that SHA. vLLM caller refs are from the composed r12 vLLM tree (base b46c3aa). Deployment context: GLM-5.2 753B MoE (hidden 6144, 78 layers, top-k 8, indexer top-k 2048), EXL3 trellis 3 bpw, TP4, FULL_AND_PIECEWISE graphs; launcher defaults VLLM_EXL3_TRELLIS_MAX_M=32, VLLM_EXL3_TRELLIS_BLOCK_M=8, VLLM_EXL3_PREFILL_CHUNK=128.
All items below are per-call Python/launch-path costs: they are paid on every eager MoE/indexer invocation (prefill steps, and any batch outside graph coverage) and are amortized to capture time inside CUDA graphs. Related: #93 (trellis scratch sizing), #94 (indexer fold slab), and the vLLM-side routing-dtype/env issue local-inference-lab/vllm#204.
1. W4A16 fused-MoE runtime resolve rebuilds the full kernel object before every compile-cache probe; the plan-time prewarm is unreachable from that probe
Where (runtime resolve, no force_tile_config): sparkinfer/moe/_shared/kernels/w4a16/kernel.py#L10248-L10277
if fused_launch is None:
fused = compile_w4a16_fused_moe(
size_m=m,
...
# (no force_tile_config here)
)
Where (vLLM bind always passes None): sparkinfer/moe/fused_moe/_impl.py#L937-L959 — TPMoEScratchPlan.bind(...) hard-codes fused_launch=None, topk_sum_launch=None, and the prewarmed launches stored at #L6455 (_prewarmed_fused_launches) are never read anywhere — they exist only to warm _FUSED_CACHE.
Mechanism, part A — per-call Python before the probe. compile_w4a16_fused_moe (kernel.py:7026) runs, on every run_w4a16_moe call with fused_launch=None:
- 2 (up to 4)
_select_tile_config sweeps (#L7161-L7216) — these run even when the result will be overridden by a pinned tile config;
- construction of a full
W4A16FusedMoeKernel, i.e. two W4A16GemmKernel inits (~300 lines of derived-field arithmetic each), including an unconditional nf3_codebook_pools(...) recompute (kernel.py:826-831) and torch.cuda.get_device_properties(torch.cuda.current_device()) per GEMM (kernel.py:894);
- only then the cache probe on
kernel.__cache_key__ (L7423-7428), and on a hit a dataclasses.replace of a ~30-field frozen result (L7430-7435). The same pattern repeats for the top-k-sum launch.
Recomputed cost: a faithful pure-Python replica of the two _select_tile_config sweeps measures ~7 us per invocation on this host; the kernel-object construction around it is several times that. Estimated total ~30-100+ us per layer per eager call, x78 layers => roughly 2-8 ms of pure Python per eager prefill step, repeated every step (amortized only inside captured graphs). vLLM profiler traces of eager GLM prefill should show this directly under compile_w4a16_fused_moe.
Mechanism, part B — the prewarm can never be hit by the runtime probe. The plan-time prewarm passes force_tile_config=core_plan.trellis_tile_config (_impl.py#L6249-L6278); vLLM pins (64, 256, 64, 256) (or (64, 128, 64, 128)) via exl3.py:_trellis_tile_config. The runtime resolve omits force_tile_config, so its tiles come from _select_tile_config — and at moe_block_size=8 the candidate list is _SMALL_BATCH_TILE_CONFIGS = ((128,128,256),(64,128,128),(128,64,128)) (#L219-L223), which does not contain the pinned (tile_k=64, tile_n=256) at all (our replica selects (128,128,256) for all GLM-shaped m). Since the fused cache key embeds the FC tile shapes, cta_threads and blocks_per_sm (kernel.py:4780-4816 via the per-GEMM key L1000-L1037), the prewarmed entries and the runtime probes live under different keys: the prewarm JIT-compiles kernels that are never launched, and the first-use CuTe JIT (100s of ms per specialization) that the prewarm comment (L6452-6454) intends to avoid still happens at warmup / first live shape.
Mechanism, part C — the exact-M prewarm threshold. _impl.py#L6239-L6247:
fused_token_counts = (
tuple(range(1, capacity_tokens + 1))
if capacity_tokens <= 32
else (capacity_tokens,)
)
At the launcher default VLLM_EXL3_TRELLIS_MAX_M=32 the decode plan prewarms every exact M (good). But the prefill plan's capacity is max_num_batched_tokens (3072/8192), so it prewarms only that single M; and any operator who raises TRELLIS_MAX_M to cover larger FULL-graph capture sizes (e.g. 64) silently flips the decode plan from "all M" to "capacity only". Because blocks_per_sm is m-dependent at small M (work-CTA clamp, kernel.py:367-369), each small M is its own cache entry — an M bucket that was neither prewarmed (part B makes that moot today) nor exercised during vLLM's warmup dummy runs pays a multi-100-ms JIT mid-serving on first occurrence.
Suggested fix (in order of impact):
- Consume the plan's prewarmed launches: have
bind() attach the matching _prewarmed_fused_launches entry instead of hard-coding fused_launch=None — this skips the entire per-call prologue and makes the prewarm meaningful.
- Failing that: pass
force_tile_config on the runtime resolve so prewarm and runtime agree on keys, and probe the cache with a key derived from the inputs before constructing kernel objects.
- Align the prewarm threshold with the plan capacity (or log when capacity > 32 truncates coverage).
2. _get_c_tmp fallback: capture raises, eager silently allocates per call — add a log-once
Where: kernel.py#L9164-L9184
if int(scratch.numel()) >= int(elements):
return scratch[: int(elements)]
if torch.cuda.is_current_stream_capturing():
raise RuntimeError(
"W4A16 GEMM scratch is not initialized for CUDA graph capture; ..."
)
return torch.empty((elements,), dtype=torch.float32, device=device)
Mechanism: if a caller-provided fc*_c_tmp binding is under-sized, capture fails loudly but eager mode silently falls into a fresh fp32 torch.empty per call per layer — allocator churn that hides the sizing bug precisely in the mode where it is cheapest to notice. Defensive today (vLLM's bindings are plan-sized), but the asymmetry means an eager-only regression would ship unnoticed. Suggested fix: logger.warning-once (with requested vs provided numel) when the fallback path is taken.
3. Paged indexer prefill route parses env per call and per supertile chunk
Where: sparkinfer/attention/nsa_indexer/paged.py#L96-L128 (fold policy, 2x os.getenv, called per index_topk_fp8 at L942->L159), #L717-L735 (supertile-K), #L1034 (per chunk), and nsa_indexer/kernel.py#L3002-L3006 (os.environ.get per run_paged_supertile_logits_kernel call at L2887).
Mechanism & scope (sharpened from our first pass): the production decode route returns at the fused early-exit (paged.py:839-865) before any env read, so decode is unaffected. The tiled/packed-contiguous route (prefill; decode only if the fused route is not resolved) pays per call: 2 getenv (fold policy) + up to 1 (supertile-K; usually short-circuited by the binding/scratch attrs at L867-870) + 2 per supertile chunk (L1034 + kernel.py:2887). Cost: 78 layers x (2 + 2 x num_chunks); at 512k context (16 chunks of 32768 tokens) that is ~2,600 os.getenv + strip/lower/int-parse per prefill step — order 1-3 ms of Python, every step, on the eager path. Fix: resolve once at plan/module init — fused_indexer.py:110 already does exactly this for its own flag, and the vLLM side has the same pattern (tracked in local-inference-lab/vllm#204).
Note on routing-id dtype (cross-ref vllm#204)
For completeness: the int64->int32 topk cast quoted in earlier drafts of this review (kernel.py:10115-10117) is gated on weight_layout in ("packed", "nf3_2p1") and m<=8, so it is unreachable on the GLM trellis3_t256 path; the route-pack/top-k-sum kernels consume int64 ids natively via route_ids_dtype-specialized launches (prewarmed for both dtypes, _impl.py:6597). The sparkinfer-side residual cost of vLLM handing int64 is therefore 2x route-id bytes plus duplicate kernel specializations, not a per-step cast. The root fix is on the vLLM side (local-inference-lab/vllm#204).
Verification hint
nsys/py-spy an eager GLM prefill step: expect visible per-layer time under compile_w4a16_fused_moe / W4A16GemmKernel.__init__ (item 1) and _read_two_level_fold_policy / os.getenv (item 3). For item 1 part B: log _FUSED_CACHE keys after plan-time prewarm vs after the first live forward — the prewarmed keys are never probed.
Found during a performance review of the v20 r12 release image
voipmonitor/vllm:gilded-gnosis-v20-vllmb46c3aa-si35aebc6-fi801d57a-cu132-20260730-r12. Code refs are against the composed r12 tree; all cited sparkinfer files are byte-identical to public commitde04125508c75e748016493f851e6bd4513b515b, so permalinks use that SHA. vLLM caller refs are from the composed r12 vLLM tree (base b46c3aa). Deployment context: GLM-5.2 753B MoE (hidden 6144, 78 layers, top-k 8, indexer top-k 2048), EXL3 trellis 3 bpw, TP4,FULL_AND_PIECEWISEgraphs; launcher defaultsVLLM_EXL3_TRELLIS_MAX_M=32,VLLM_EXL3_TRELLIS_BLOCK_M=8,VLLM_EXL3_PREFILL_CHUNK=128.All items below are per-call Python/launch-path costs: they are paid on every eager MoE/indexer invocation (prefill steps, and any batch outside graph coverage) and are amortized to capture time inside CUDA graphs. Related: #93 (trellis scratch sizing), #94 (indexer fold slab), and the vLLM-side routing-dtype/env issue local-inference-lab/vllm#204.
1. W4A16 fused-MoE runtime resolve rebuilds the full kernel object before every compile-cache probe; the plan-time prewarm is unreachable from that probe
Where (runtime resolve, no
force_tile_config):sparkinfer/moe/_shared/kernels/w4a16/kernel.py#L10248-L10277Where (vLLM bind always passes None):
sparkinfer/moe/fused_moe/_impl.py#L937-L959—TPMoEScratchPlan.bind(...)hard-codesfused_launch=None, topk_sum_launch=None, and the prewarmed launches stored at#L6455(_prewarmed_fused_launches) are never read anywhere — they exist only to warm_FUSED_CACHE.Mechanism, part A — per-call Python before the probe.
compile_w4a16_fused_moe(kernel.py:7026) runs, on everyrun_w4a16_moecall withfused_launch=None:_select_tile_configsweeps (#L7161-L7216) — these run even when the result will be overridden by a pinned tile config;W4A16FusedMoeKernel, i.e. twoW4A16GemmKernelinits (~300 lines of derived-field arithmetic each), including an unconditionalnf3_codebook_pools(...)recompute (kernel.py:826-831) andtorch.cuda.get_device_properties(torch.cuda.current_device())per GEMM (kernel.py:894);kernel.__cache_key__(L7423-7428), and on a hit adataclasses.replaceof a ~30-field frozen result (L7430-7435). The same pattern repeats for the top-k-sum launch.Recomputed cost: a faithful pure-Python replica of the two
_select_tile_configsweeps measures ~7 us per invocation on this host; the kernel-object construction around it is several times that. Estimated total ~30-100+ us per layer per eager call, x78 layers => roughly 2-8 ms of pure Python per eager prefill step, repeated every step (amortized only inside captured graphs). vLLM profiler traces of eager GLM prefill should show this directly undercompile_w4a16_fused_moe.Mechanism, part B — the prewarm can never be hit by the runtime probe. The plan-time prewarm passes
force_tile_config=core_plan.trellis_tile_config(_impl.py#L6249-L6278); vLLM pins(64, 256, 64, 256)(or(64, 128, 64, 128)) viaexl3.py:_trellis_tile_config. The runtime resolve omitsforce_tile_config, so its tiles come from_select_tile_config— and atmoe_block_size=8the candidate list is_SMALL_BATCH_TILE_CONFIGS = ((128,128,256),(64,128,128),(128,64,128))(#L219-L223), which does not contain the pinned(tile_k=64, tile_n=256)at all (our replica selects(128,128,256)for all GLM-shaped m). Since the fused cache key embeds the FC tile shapes,cta_threadsandblocks_per_sm(kernel.py:4780-4816 via the per-GEMM key L1000-L1037), the prewarmed entries and the runtime probes live under different keys: the prewarm JIT-compiles kernels that are never launched, and the first-use CuTe JIT (100s of ms per specialization) that the prewarm comment (L6452-6454) intends to avoid still happens at warmup / first live shape.Mechanism, part C — the exact-M prewarm threshold.
_impl.py#L6239-L6247:At the launcher default
VLLM_EXL3_TRELLIS_MAX_M=32the decode plan prewarms every exact M (good). But the prefill plan's capacity ismax_num_batched_tokens(3072/8192), so it prewarms only that single M; and any operator who raisesTRELLIS_MAX_Mto cover larger FULL-graph capture sizes (e.g. 64) silently flips the decode plan from "all M" to "capacity only". Becauseblocks_per_smis m-dependent at small M (work-CTA clamp, kernel.py:367-369), each small M is its own cache entry — an M bucket that was neither prewarmed (part B makes that moot today) nor exercised during vLLM's warmup dummy runs pays a multi-100-ms JIT mid-serving on first occurrence.Suggested fix (in order of impact):
bind()attach the matching_prewarmed_fused_launchesentry instead of hard-codingfused_launch=None— this skips the entire per-call prologue and makes the prewarm meaningful.force_tile_configon the runtime resolve so prewarm and runtime agree on keys, and probe the cache with a key derived from the inputs before constructing kernel objects.2.
_get_c_tmpfallback: capture raises, eager silently allocates per call — add a log-onceWhere:
kernel.py#L9164-L9184Mechanism: if a caller-provided
fc*_c_tmpbinding is under-sized, capture fails loudly but eager mode silently falls into a fresh fp32torch.emptyper call per layer — allocator churn that hides the sizing bug precisely in the mode where it is cheapest to notice. Defensive today (vLLM's bindings are plan-sized), but the asymmetry means an eager-only regression would ship unnoticed. Suggested fix:logger.warning-once (with requested vs provided numel) when the fallback path is taken.3. Paged indexer prefill route parses env per call and per supertile chunk
Where:
sparkinfer/attention/nsa_indexer/paged.py#L96-L128(fold policy, 2xos.getenv, called perindex_topk_fp8at L942->L159),#L717-L735(supertile-K),#L1034(per chunk), andnsa_indexer/kernel.py#L3002-L3006(os.environ.getperrun_paged_supertile_logits_kernelcall at L2887).Mechanism & scope (sharpened from our first pass): the production decode route returns at the fused early-exit (paged.py:839-865) before any env read, so decode is unaffected. The tiled/packed-contiguous route (prefill; decode only if the fused route is not resolved) pays per call: 2 getenv (fold policy) + up to 1 (supertile-K; usually short-circuited by the binding/scratch attrs at L867-870) + 2 per supertile chunk (L1034 + kernel.py:2887). Cost: 78 layers x (2 + 2 x num_chunks); at 512k context (16 chunks of 32768 tokens) that is ~2,600
os.getenv+ strip/lower/int-parse per prefill step — order 1-3 ms of Python, every step, on the eager path. Fix: resolve once at plan/module init —fused_indexer.py:110already does exactly this for its own flag, and the vLLM side has the same pattern (tracked in local-inference-lab/vllm#204).Note on routing-id dtype (cross-ref vllm#204)
For completeness: the int64->int32 topk cast quoted in earlier drafts of this review (kernel.py:10115-10117) is gated on
weight_layout in ("packed", "nf3_2p1")and m<=8, so it is unreachable on the GLM trellis3_t256 path; the route-pack/top-k-sum kernels consume int64 ids natively viaroute_ids_dtype-specialized launches (prewarmed for both dtypes,_impl.py:6597). The sparkinfer-side residual cost of vLLM handing int64 is therefore 2x route-id bytes plus duplicate kernel specializations, not a per-step cast. The root fix is on the vLLM side (local-inference-lab/vllm#204).Verification hint
nsys/py-spy an eager GLM prefill step: expect visible per-layer time undercompile_w4a16_fused_moe/W4A16GemmKernel.__init__(item 1) and_read_two_level_fold_policy/os.getenv(item 3). For item 1 part B: log_FUSED_CACHEkeys after plan-time prewarm vs after the first live forward — the prewarmed keys are never probed.