mirror of
https://github.com/kvcache-ai/ktransformers.git
synced 2026-11-05 01:07:28 +00:00
[GH-ISSUE #2172] [MXFP4 layerwise prefill] SM 8.6 (Ampere) excluded by capability whitelist — DSV4-Flash on dual A10 falls back to serialized full-GPU path #1295
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
kvcache-ai/ktransformers#1295
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @1stlook-dev on GitHub (Aug 26, 2026).
Original GitHub issue: https://github.com/kvcache-ai/ktransformers/issues/2172
Summary
While evaluating KTransformers (DSV4-Flash Day-0 support) for a dual-NVIDIA-A10 (Ampere, SM 8.6) deployment, I noticed that the MXFP4 layerwise prefill pipeline is enabled only on Ada/Blackwell.
python/sglang/srt/layers/moe/kt_ep_wrapper.py(in thethird_party/sglangsubmodule, branchmain):SM 8.6 (A10/A30/A40/A6000/L40) is excluded, and there is no env override to opt in (the
SGLANG_V4_USE_TRITON_KERNELSdiagnostic only disables the pipeline).Impact
On Ampere, when
--kt-gpu-prefill-token-threshold > 0(the DSV4-Flash doc uses4096), a prefill request at/above the threshold enters the_full_gpu_gatebranch, but since_mxfp4_pipeline_runtime_supported()is False it falls through to:So each layer does: H2D-load all 256 experts -> compute -> hard sync -> next layer. That is a serialized per-layer transfer chain — the exact bottleneck we are trying to escape (see below). As the gate stands, Ampere deployments get no benefit from the layerwise pipeline.
Background / motivation
Our current DSV4-Flash deployment (vLLM + CPU-MoE backend, TP2 on 2x A10) plateaus at ~650 tok/s cold prefill, independent of prompt length (8k-64k). Thread-level diagnosis attributes it to a latency-bound serialized per-layer weight-transfer chain (1-3 runnable threads on the transfer side, zero threads blocked on bandwidth; GPU memory util 14-44% during prefill — i.e. the GPU idles waiting on H2D, not on compute).
We want to evaluate whether KTransformers' double-buffered layerwise prefill can break that plateau, but with the current capability gate we would land on the same serialized-chain shape on SM 8.6.
Questions
SGLANG_V4_MXFP4_LAYERWISE_FORCE=1)?Environment
approachingai/ktransformers:DSV4-specific/ main branch@wenmin-wu commented on GitHub (Aug 26, 2026):
Regarding Q1 ("unverified/incorrect on Ampere, or simply untested?") — I went through the kernel sources rather than the Python gate, and everything I can find points to untested rather than unsupported. Sharing the trace in case it saves the maintainers a pass; all of it is verifiable from
main.The Python whitelist is narrower than the kernel's own requirement.
python/sglang/srt/layers/moe/kt_ep_wrapper.py:3228while the CUDA side gates only on:
python/sglang/jit_kernel/csrc/gemm/marlin_moe/moe_wna16_marlin.cuh:37→#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ < 800(stub below 8.0, real implementation otherwise)python/sglang/jit_kernel/csrc/gemm/marlin_moe/marlin_template.h:37→ same guardpython/sglang/jit_kernel/csrc/gemm/marlin/gptq_marlin_repack.cuh:32→ same guardSo the compiled artifacts declare sm_80+, and SM 8.6 sits inside that.
The tensor-core ISA in the MoE template is Ampere-era. Grepping
marlin_template.hfor every PTX/intrinsic it emits returns exactly:No
wgmma, no TMA, no cluster barriers, nocvt.*to/from FP8/FP4, and no__CUDA_ARCH__branch on 900/1000/1200. MXFP4 is dequantized into 16-bit and multiplied on tensor cores that Ampere already has — which is consistent with Marlin's original design target.The repack path looks arch-neutral too.
mxfp4_marlin_repackcarries the same< 800guard and emits no FP4/FP8 conversion instructions; it treats MXFP4 as packed bytes, so there is no Ada/Blackwell-only numerics step I can see.Shared memory is already handled dynamically, which was my main a-priori worry for GA10x (100 KB opt-in per block vs A100's 164 KB):
moe_wna16_marlin.cuh:692→cudaDeviceGetAttribute(&max_shared_mem, cudaDevAttrMaxSharedMemoryPerBlockOptin, dev)marlin_template.h:804→sh_a_max_row = ((max_shared_mem - 1024) / 16 - shm_size_used) / (thread_k_blocks * 2)i.e. the tiling adapts to whatever budget the device reports rather than assuming a fixed size.
And the GEMM is JIT-compiled (
load_jit("moe_wna16_marlin", ..., cuda_files=["gemm/marlin_moe/moe_wna16_marlin.cuh"])), so there is no prebuilt sm_89 cubin that SM 8.6 would be missing.To be explicit about what this is and is not: the above is a source read, not a benchmark. I have not run the layerwise pipeline on Ampere — the gate stops it before it can be observed, which is exactly why the question is worth asking.
Offer for Q3. If it is a verification gap, we can contribute a second, independent SM 8.6 data point that is deliberately different from the OP's box on the CPU side:
Happy to run whatever certification matrix you specify — correctness (logit/output parity against the serialized full-GPU fallback), prefill throughput vs prompt length, and MXFP4 numerical checks. An opt-in escape hatch such as
SGLANG_V4_MXFP4_LAYERWISE_FORCE=1(as the OP suggests) would be enough for us to produce those numbers without patching the submodule; today the only knob,SGLANG_V4_USE_TRITON_KERNELS, can just disable the pipeline.One narrower question, in case the whitelist is deliberate rather than conservative: is there a known correctness issue specific to
mma.m16n8k16without the Ada/Blackwell FP8 path — e.g. an accumulate-order or scale-swizzle assumption in_swizzle_e8m0_scalesthat only holds on 8.9+? That is the one thing a source read cannot rule out, and it would explain the gate.@1stlook-dev commented on GitHub (Aug 27, 2026):
Good catch — and this matches what I can verify in the sglang submodule pinned by KTransformers: the same
((8, 9), (12, 0))check (kt_ep_wrapper.py:2450in our checkout,:3228onmain), and the same< 800stub/real split inmoe_wna16_marlin.cuh(empty stub at :37, real kernel under#else:51–:822) andmarlin_template.h— only Ampere tensor-core intrinsics (mma.sync.aligned.m16n8k16,ldmatrix), no wgmma/TMA/FP8 cvt. So: the gate is narrower than the kernel requires, and the only reason it has never been observed on Ampere is that the gate stops it first.One clarification for triage on where the OP's box actually dies — the GPU gate was not the blocker on our machine. 2x A10 (sm86), 2x EPYC 7H12 AVX2-only (no AVX-512/AMX), NPS4: 32 NUMA nodes x ~32GB:
OOM on the default topology. Default is
numa_ids=[0,1]+ process-widenuma_bind()(MPOL_BIND), so each rank's CPU-expert working set (raw + swizzled MXFP4 both resident while--kt-gpu-prefill-token-threshold > 0, ~140GB/rank) is hard-pinned into node0's 32GB → OOM-kill at layer 17 (sglang::scheduler, anon-rss ~60GB and still growing). We relaxed the bind tonuma_set_localalloc()and spread 16 pools across nodes 16–31: the OOM went away and TP1 completed the full 43-layer load (28.75s).Then the AVX2 worker pool crashes, before the GPU MoE path is ever reached. With 16 pools: segfault in layer-0
load_weights(kt-kernelamx.py:936, work-stealing) — a worker thread jumps to0xffffffffffffffff. With a 2-pool control: it dies earlier, in pool construction —Failed to set thread name: Numerical result out of range,Core 27 inside NUMA node 16 not found,malloc(): unsorted double linked list corrupted→ abort. The control proves it is not our memory-policy change: theWorkerPoolmachinery itself (thread naming / core enumeration / heap) assumes few NUMA nodes and breaks on 32.So the current data point for sm86 + AVX2-only + NPS4/32-node is: weights load fine (43/43 layers on one rank), but the box cannot get past layer-0 CPU MoE.
Your offer complements ours nicely: between the two boxes, sm86 would be covered for both AVX-512 (your 2-node Xeon) and AVX2-only (our 32-node EPYC) CPU backends, across both 2-node and 32-node topologies. Happy to share the raw crash logs or the patched
worker_pool.hif it helps.On your narrower question — whether
mma.m16n8k16without the Ada/Blackwell FP8 path has a correctness issue specific to 8.9+ — we can't speak to it: our box dies before any GPU MoE kernel runs, so there is no output to compare against.