Repository navigation
feat(rdna2): opt-in W8A8 int8 sdot4 dense and MoE kernels for gfx1030 - #93
Draft
BlivionIaG wants to merge 1 commit into
Draft
BlivionIaG wants to merge 1 commit into
BlivionIaG wants to merge 1 commit into
Conversation
int8 weights x int8 activations through v_dot4_i32_i8, the int8-weight sibling of the W4A8 sdot4 pair, for compressed-tensors W8A8 int8 checkpoints (symmetric per-channel weights, dynamic per-token activations). Default off; VLLM_RDNA2_W8A8_SDOT4=1 opts in. - csrc/rocm/w8a8_sdot4_rdna2.cuh: shared mac_window loop, dense w8a8_gemm_kernel and fused moe_w8a8_gemm_kernel. Activations reuse w4a8_act_quant_kernel unchanged; weights are repacked at load into a K-major dword layout in the act tile's byte order ([K/4, N] int32). - csrc/rocm/w8a8_sdot4_rdna2.cu, moe_w8a8_rdna2.cu: torch ops. Dense serves all M (split-K into an fp32 scratch at small M); MoE keeps moe_w4a8_gemm_rdna2's contract and accumulation modes. - RDNA2Int8ScaledMMLinearKernel (first int8 scaled-mm kernel on ROCm) and CompressedTensorsW8A8Int8RDNA2MoEMethod (selected ahead of the generic int8 MoE method), both gated on the env and gfx1030. - Tests, tools/rdna/bench_moe_w8a8.py, docs/rdna2/w8a8-wiring.md. The quantization scheme follows curvedinf/int8-vllm (per-channel int8 weights, round-to-nearest per-token int8 activations, int8 on every large dense GEMM), read at commit c7fe970 from its README.md, docs/recipes/README.md and INT8_AUDIT_RESULTS.md. No code is taken from it: its GEMMs are gfx908 CK/AITER and Triton kernels. Not built or run on hardware: host syntax check with a HIP builtin shim, NumPy emulation of the pack/tile pairing, ruff, clang-format and mypy only. Inspired-by: curvedinf/int8-vllm@c7fe970 (W8A8 scheme; no code copied) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147zxyqwsKMmBLRn8xgBaHL
BlivionIaG
force-pushed
the
claude/w8a8-sdot4-rdna2
branch
from
October 13, 2026 07:33
046b096 to
2ef18c3
Compare
4 tasks done
This was referenced Oct 23, 2026
6 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
Opt-in W8A8 int8 (int8 weights x int8 activations) dense and MoE GEMMs for gfx1030 through
v_dot4_i32_i8(sdot4), the int8-weight sibling of the W4A8 sdot4 pair (w4a8_sdot4_rdna2.cu/moe_w4a8_rdna2.cu). Serves compressed-tensors W8A8 int8 checkpoints (symmetric per-channel weights, dynamic per-token activations) — the scheme curvedinf/int8-vllm runs its Qwen3.8-27B W8A8 recipe with (per-channel weights, round-to-nearest per-token activation quant, all large dense GEMMs in int8). Their GEMMs are AITER/CK MFMA kernels for MI100; gfx1030 has no MFMA, so this is the sdot4 equivalent. Default OFF: nothing changes unlessVLLM_RDNA2_W8A8_SDOT4=1.Attribution. The quantization scheme and the "which layers run int8" choices follow curvedinf/int8-vllm at commit
c7fe970(author curvedinf), read from itsREADME.md,docs/recipes/README.mdandINT8_AUDIT_RESULTS.md. No code was copied from that repository (its kernels are gfx908 CK/AITER and Triton). The commit carries anInspired-by:trailer with the same pin; the kernel header anddocs/rdna2/w8a8-wiring.mdrepeat it.Kernels (
csrc/rocm/w8a8_sdot4_rdna2.cuh, builtin-only like the W4A8 header)w4a8_act_quant_kernel(PER_GROUP, MT=8) writes per-(token, group) int8 + f32 scales.rdna2_w8a8.pack_weight) into[K/4, N]int32: per 8-K chunk two dword rows per column in the act tile'skAPermbyte order, soa8.xpairs with row2c,a8.ywith row2c+1, and oneu32x4load brings 4 columns x 4 K.mac_window<ROWS, AG>:w4a8_gemm_kernel's loop with the nibble split replaced by the second dword row; i32 accumulate per activation group, flush to f32 with the activation scale and the weight scale (fetched per group atk / w_group, so per-channel and per-group weight scales share the loop). No zero points.w8a8_gemm_kernel<AG>: grid(ceil(N/1024), ceil(M/8), split_k); K walked in 256-wide windows staged through 2 KiB LDS (no LDS cap, nopick_split_kmirror). All M run here (no int8 fallback exists for this layout): below 256 blocks the host splits K into a zeroed fp32 scratch (fp32 atomics + one cast); prefill-sized M keeps plain fp16 stores.moe_w8a8_gemm_kernel<BLOCK_M, AG, C_T>:moe_w4a8_gemm_kernel's grid, staging and epilogue (router weight in fp32, fp32 scratch atomics or packed fp16 CAS,output_topkfused reduce).Ops
w8a8_gemm_rdna2(a, w_packed, w_scales, a_group) -> fp16 [M, N]andmoe_w8a8_gemm_rdna2(..., a_group, fp32_accum)(moe_w4a8's contract). BothTORCH_CHECKon ineligible shapes rather than falling back.Wiring
RDNA2Int8ScaledMMLinearKernel(scaled_mm/rdna2_w8a8_int8.py), first in_POSSIBLE_INT8_KERNELS[ROCM];is_supported= env + gfx1030 + op built,can_implementdeclines static input scales and asymmetric activations. Packs the weight and expands per-tensor scales to per-channel (fused modules) at load.CompressedTensorsW8A8Int8RDNA2MoEMethod, selected inget_moe_methodviarocm_moe_rdna.is_supported_w8a8_int8before the generic int8 MoE method. Reuses the parent's checkpoint-layoutcreate_weights, packs inprocess_weights_after_loading,applymirrors the W8A16 RDNA2 method.docs/rdna2/w8a8-wiring.md(contract, design, validate steps), atuning.mdrow,tools/rdna/bench_moe_w8a8.py(W8A8 vs W8A16 int8 MoE micro A/B).Not a duplicate. The fork's int8 work is weight-only:
VLLM_RDNA_DENSE_INT8shadows +gemv_i8_rdna2(#56, W8A16 decode GEMV),moe_w8a16_gemm_rdna2(W8A16 MoE), and #71/#73 are int8 KV cache. No open or closed PR has an int8 x int8 GEMM; #9 (closed, W4A8 explore) listed "W8A8 INT8 sdot4" as a dependency that did not exist. No PR touches_POSSIBLE_INT8_KERNELSor the W8A8 int8 MoE method selection.AI assistance. Written with AI assistance (Claude Code). A human submitter must review every changed line and run the gfx1030 steps below before this leaves draft.
Test Plan
Done here (no GPU, no hipcc in this sandbox):
On gfx1030 (V620), to do before merge:
PYTORCH_ROCM_ARCH=gfx1030; confirm_rocm_C::w8a8_gemm_rdna2and_rocm_C::moe_w8a8_gemm_rdna2intorch._C._jit_get_all_schemas().pytest tests/kernels/quantization/test_rdna2_w8a8.py tests/kernels/quantization/test_rdna2_moe_w8a8.py -v(the three packing tests run on CPU with torch; the rest need the GPU).v_dot4c_i32_i8in thew8a8symbols,sgpr_spill_count/vgpr_spill_count0, no scratch.python tools/rdna/bench_moe_w8a8.py— W8A8 vs the W8A16 int8 MoE kernel per token count, with the relative error.VLLM_RDNA2_W8A8_SDOT4=0/1:RDNA2 W8A8 sdot4 dense/MoE path activeonce per worker, greedy probes, PPL on/off, prefill and decode tok/s at 1k/512 and 16k/1k, c=1 and c=8.Test Result
m,k,n,ag,wg= 3/64/8/32/64, 9/256/12/128/256, 2/128/4/64/64, 1/96/8/32/96).rdna_extras.ops.h40 pre-existing violations before and after,torch_bindings.cpp2 before and after.Design points a reviewer should weigh (all opt-in, none change a default): all-M dense serving (decode through split-K fp32 atomics instead of the T45
gemv_i8_rdna2design), the 256-K LDS window instead of W4A8's whole-split staging, and no zero-point support (GPTQ-style int8 withqzerosstays onmoe_w8a16_gemm_rdna2).Pointers: what to explore and test once it builds
Roughly in the order the risk sits. Everything here is a hypothesis until the V620 says otherwise.
Correctness risks specific to this design
__hip_atomic_fetch_add(float*). On gfx1030 this should lower toglobal_atomic_add_f32; if the ISA dump shows a CAS loop instead, decode will crawl. Check the.s(orllvm-objdump -don_rocm_C.so) forglobal_atomic_add_f32in thew8a8symbols; the fix is-munsafe-fp-atomicson these two TUs oratomicAddNoRet, asmoe_accum_rdna2.cuhrelies on.at::emptyfor the act workspace,at::zerosfor the fp32 scratch). PyTorch pools these inside the graph, butdocs/rdna2/w4a8-moe-wiring.mdrecords a suspected graph-aliasing issue from exactly this pattern. Test:FULL_AND_PIECEWISEserve, greedy output identical across replays and across batch sizes 1/2/4/8. If it misbehaves, move to a persistent per-(M, K, AG, device)workspace likemoe_fp32_scratch.K % 256 != 0(tail window is a whole number of activation groups), but the pytest shapes are all multiples of 256. Add a case likeK=1120(a_groupfalls to 32) andK=2304witha_group=128(9 windows); the NumPy emulation covered a tail but the kernel'schunks < 32staging branch has not run on silicon.a.stride(0) % 8 == 0is required (16-byteu32x4loads in the act quant). Thescaled_mmpath reshapesxwithout.contiguous(); a sliced input with an odd row stride would TORCH_CHECK rather than silently miscompute — confirm which callers reach it (QKV splits,lm_headslices) and whether a.contiguous()is cheaper than the check firing.expert_map(expert_id == -1early-out),apply_router_weight_on_input=True, non-gated activations (is_gated=False),top_k=1, andnum_tokensjust above the 16-token decode buffers.VLLM_LAYER_NAN_DEBUG.Performance questions (measure before tuning)
gemv_i8_rdna2(T45). The all-M design runs decode through the 8-row tile with 7 padding rows and split-K atomics; the fork's int8 decode GEMV (VLLM_RDNA_DENSE_INT8) streams rows wave-per-row with fp16 activations. At the Flash-Next dense shapes (GDN in/out proj, QSA qkv/o, shared expert, lm_head) time both at M = 1, 4, 8. If the GEMV wins, the options are routing M <= 8 to a W8A16 path (needs the[N, K]layout: a second copy, or an unpack-on-the-fly), or a dedicated decode kernel over the packed layout with a wave-level K split and LDS reduce (no atomics, deterministic).kTargetBlocks = 256, windows of 256 K. At M=1, N=5120, K=5120 the grid is 5 x 1 x 20. Sweep the target (128/256/512) and the window (128/256/512); the split only exists for occupancy, so look at achieved bandwidth vs the 26 MB weight read.sdot4, so expect a smaller margin. Find the M where W8A8 crosses the TunableOp rocBLAS row at the recipe shapes (M = 33 … 2048, 1k/16k chunks). Below the crossover the kernel class should hand M to rocBLAS on a dequantized weight, or the registry should keep Triton.wx[4][4] + wy[4][4]are 16 VGPRs more than W4A8'swv[4][4]; the W4A8 MoE instantiations sat at 57–168 VGPRs. Read the counts from the ISA audit; ifBLOCK_M = 8lands above 128 (fewer than 4 waves/SIMD at 256 threads), tryK_STEP = 16(DW = 2) orNPT = 2variants.ec90fc933), removes a launch per layer at decode.block_size_mis 1 / 4 / 8 at 4 / 512 tokens; reuse theVLLM_RDNA2_MOE_BSM_LARGE/_BSM_MIN_TOKENSknobs rather than the constants, and compare against bothmoe_w8a16_gemm_rdna2(tools/rdna/bench_moe_w8a8.py) and the W4A8 MoE at equal token counts.Accuracy
act_group(K)takes the largest of 128/64/32 dividing K. Smaller groups cost one more flush per group but localize outlier channels (llama.cpp Q8_1); int8-vllm quantizes per whole token. Measure PPL at AG = 32/64/128 against fp16 and against a per-token reference to see whether the extra scales buy anything.in_proj_a/b, norms, conv, attention softmax/PV and the drafterfcin float and runslm_headint8 behind a flag. Replicate with the compressed-tensorsignorelist on a Flash-Next W8A8 export and A/B each exclusion; their one-shot GPTQ-int8 lost KLD 0.011 vs 0.0069 after retraining, so expect the checkpoint recipe to matter as much as the kernel.W8A8export of Qwen3-0.6B/1.7B quantizes in minutes and exercises both the dense kernel and (on a small MoE) the MoE method before the 27B-class run.VLLM_RDNA2_MOE_FP32_ACCUM(fp32 scratch) vs the packed-fp16 CAS epilogue for run-to-run stability, as documented for the other MoE kernels.Follow-ups this PR deliberately leaves out
W8A8-GS128exports are GPTQ) storeuint8 + qzeros. The W4A8 zero fold (acc = -z * asum;asumis already produced by the act quant and currently ignored) carries over unchanged, plus a loader from the W8A16 MoE layout. The kernel already takes a runtime weight group, so GS128 scales need no kernel change.can_implementcurrently declines them.custom_all_reduce_int8_gs32.hip/decode_int8_intermediates.hip(int8 payload all-reduce fused with residual + RMSNorm at TP4) is a separate exploration forrdna_ar([backlog] perf(rdna): raise the one-shot rdna_ar gate 64 -> 128-160 KiB #60, [backlog] perf(rdna): reduce-scatter + allgather custom all-reduce (rdna_ars) #61).Essential Elements of an Effective PR Description Checklist
supported_models.mdandexamplesfor a new model. (docs/rdna2/w8a8-wiring.md,tuning.md)🤖 Generated with Claude Code
https://claude.ai/code/session_0147zxyqwsKMmBLRn8xgBaHL