Skip to content

feat(fp8): E4M3 fused dequant-linear kernel + FP8 quantized-linear slice (RFC 0001) - #526

Open
CAICAIIs wants to merge 1 commit into
inclusionAI:mainfrom
CAICAIIs:perf/e4m3-cuda-kernel
Open

feat(fp8): E4M3 fused dequant-linear kernel + FP8 quantized-linear slice (RFC 0001)#526
CAICAIIs wants to merge 1 commit into
inclusionAI:mainfrom
CAICAIIs:perf/e4m3-cuda-kernel

Conversation

@CAICAIIs

Copy link
Copy Markdown
Contributor

Summary

Adds the FP8 weight-quantization vertical slice (RFC 0001, M1) and an E4M3 fused dequant-linear CUDA kernel for the memory-bound decode path.

Decode re-reads the full weight tensor from HBM for every generated token, so it is memory-bandwidth-bound. Storing the weight as 1-byte E4M3 FP8 halves the weight bytes read, which is the memory-bound benefit from RFC 0001. On Hopper / H20 the built-in torch._scaled_mm FP8 path delivers 1.53–1.72× decode speedup vs bf16; on Ampere / A100 (which has no built-in FP8 matmul) a custom E4M3 dequant-linear kernel is provided.

quant_method is opt-in and unchanged by default ("none"); unmarked weights keep the existing bf16 path.

Motivation

On the 8×A100 node, decode is weight-read-bound and compute sits idle. Quantizing weights to FP8 (1 byte) vs bf16 (2 bytes) halves the weight-read bytes. This PR lands the M1 vertical slice: an opt-in FP8 decode path + an Ampere E4M3 kernel, both forward-only.

Changes

  • areno/accel/csrc/e4m3_linear.cu — E4M3 fused dequant-linear. A branchless bit-trick decode (validated byte-for-byte vs torch.float8_e4m3fn) plus:
    • a small-M (M <= 4) memory-streaming GEMV: one warp streams one weight row with coalesced 8-byte loads, reuses the staged activation, and reduces with warp shuffles;
    • a WMMA (fp16 16×16×16) tensor-core GEMM fallback for M > 4.
      Per-tensor scalar scale applied once post-dot.
  • areno/accel/kernels/e4m3_cuda.pyquantized_e4m3_linear_cuda shim + quantize/dequant helpers, with an out-buffer reuse for the hot decode path.
  • areno/engine/quantization.py — FP8 (E4M3) scale/quantize/dequant reference, CPU-portable.
  • areno/engine/layers/linear.pyQuantizedLinear (W8A16 dequant-forward reference) + _areno_linear_forward routes FP8-marked weights; unmarked weights unchanged.
  • areno/engine/{config,modeling,worker}.pyquant_method / quant_group_size (opt-in), quantize_model_weights_fp8, and a guard that rejects quant_method='fp8' in a train worker (forward-only, no backward).
  • areno/accel/kernels/fp8_linear.py — the model's E5M2 W8A16 Triton dequant-linear (the grid Triton accepts on Ampere).
  • setup.py / areno/accel/csrc/extension.cpp — register the new kernel (optional out arg).
  • Teststests/test_e4m3_decode_cpu.py, tests/test_fp8_quant_cpu.py (decode formula vs torch, quant/dequant round-trip, QuantizedLinear reference, opt-in default).
  • Benchmarksscripts/bench/{e4m3_cuda_bench,h20_fp8_scaled_mm,fp8_end_to_end_bench,triton_fp8_matmul_bench}.py.
  • docs/rfcs/0001-weight-quantization-fp8-int4.md — design, gates, measured results.

Testing

  • CPU: pytest tests/test_e4m3_decode_cpu.py tests/test_fp8_quant_cpu.py11 passed.
  • GPU (A100): kernel correct vs bf16 dequant reference — max-rel err ~0.3–0.4% across M ∈ {1, 2, 4, 64}; CUDA build clean.
  • Decode benchmark:
    • A100: E4M3 GEMV is memory-bound (raw read-only kernel ≈1248 GB/s ≈ 2.1× ceiling; the GEMV's practical ceiling ≈1.68× at the bare-kernel level).
    • H20 / Hopper: built-in torch._scaled_mm FP8 E4M3 → 1.53–1.72× vs bf16 (RFC §4.8).

Known limitations

  • Forward-only: FP8 (E4M3/E5M2) has no backward, so it is decode-only and cannot train (guarded in worker.py).
  • Ampere grid: Triton rejects E4M3 on A100, so the model's runtime FP8 decode uses E5M2 (coarse ~12 %); E4M3 is the dedicated Ampere kernel and the Hopper _scaled_mm path.
  • The Ampere E4M3 GEMV is memory-bound at the bare-kernel level, but through the torch/ATen extension a per-call dispatch overhead dominates the fast kernel, so end-to-end on A100 is near parity. The >1.5× number is the Hopper _scaled_mm result.

Related

@CAICAIIs
CAICAIIs force-pushed the perf/e4m3-cuda-kernel branch 3 times, most recently from 1cebd8f to 5d8b944 Compare August 26, 2026 17:53
Add a forward-only WMMA (16x16x16 fp16 tensor-core) kernel that reads a
1-byte uint8 E4M3 weight payload, decodes each byte to fp16 in shared
memory (fast bit-decode), and applies a per-tensor scalar scale after the
dot -- the memory-bandwidth benefit from RFC 0001 for decode/inference.

- csrc/e4m3_linear.cu: fused dequant-GEMM + registration.
- kernels/e4m3_cuda.py: CUDA shim (quantized_e4m3_linear_cuda) + quantize/dequant helpers.
- tests/test_e4m3_decode_cpu.py: CPU decode reference vs torch.float8_e4m3fn.
- setup.py: compile the new .cu.

Forward-only (no backward): E4M3 must not be wired into a training graph.
Opt-in and backward-compatible; the model hook is not yet switched to this path.
@CAICAIIs
CAICAIIs force-pushed the perf/e4m3-cuda-kernel branch from 5d8b944 to 837437c Compare August 27, 2026 01:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant