perf: finalize fp8 scale rings inside quantize kernels

- last-block epilogue (threadfence + counter elect) folds amax into hist[idx], reduces the window and publishes the next scale on device — zero extra launches per linear layer
- _ScaleRing packs [hist | scale | counter] into one CUDA buffer; the eager hist-write / max / scale-copy chain and update() are gone
- split FP8QuantizeParams out of FP8Params so each operator owns its fields; linear_forward/backward_fp8 take optional ring arguments
- e2e 12L/dim1024/B4xT512 (fused AdamW): fp8 137.8ms/step vs bf16 210.3ms, 1.53x; fwd 1.82x, bwd 1.50x
This commit is contained in:
2026-08-25 11:12:14 +08:00
parent 998b443aa3
commit 4dc5e923e0
6 changed files with 324 additions and 84 deletions
+56 -5
View File
@@ -86,6 +86,51 @@ def test_quantize_bf16_e5m2_format():
torch.testing.assert_close(amax, x.abs().amax().float().reshape(1))
@skip_no_fp8
def test_quantize_ring_in_kernel_finalize():
"""The quantize kernel finalizes the delayed-scaling ring in-kernel: the
measured amax lands in hist[idx], the window reduces to the next step's
scale on device, and the counter re-arms for the next launch."""
from astrai.extension.fp8 import _ScaleRing
torch.manual_seed(21)
dev = torch.device("cuda")
ring = _ScaleRing(dev, DelayedScaling(history_len=4, margin=0))
x0 = torch.randn(256, 256, device=dev, dtype=torch.bfloat16)
w = torch.randn(256, 256, device=dev, dtype=torch.bfloat16)
sw = torch.tensor([1.0], device=dev)
ring.seed(x0, "e4m3")
hist0 = ring.hist.clone()
# Step over three fresh tensors: each launch folds its amax into
# hist[idx] and publishes max(hist)/448 as the next scale.
idx = 0
for _ in range(3):
x = torch.randn(256, 256, device=dev, dtype=torch.bfloat16) * (2.0 + 4.0 * _)
_ = linear_forward_fp8(
x,
w,
None,
ring.scale,
sw,
"e4m3",
None,
ring.state,
idx,
0,
)
torch.cuda.synchronize()
expected_hist = hist0.clone()
expected_hist[idx] = x.abs().amax().float()
torch.testing.assert_close(ring.hist, expected_hist)
expected_scale = (expected_hist.max() / 448.0).reshape(1)
torch.testing.assert_close(ring.scale, expected_scale, rtol=1e-6, atol=1e-12)
# counter re-armed to int32 zero
assert ring.state[-1].view(torch.int32).item() == 0
hist0 = expected_hist.clone()
idx = (idx + 1) % 4
@skip_no_fp8
def test_fp8_linear_forward_and_backward():
torch.manual_seed(7)
@@ -196,9 +241,9 @@ def test_fp8_linear_backward_outside_autocast():
calls = {"bwd": 0}
orig = f8mod.linear_backward_fp8
def spy(g, xx, ww, masks, sg, sw, sx, fmt="e5m2"):
def spy(*args, **kwargs):
calls["bwd"] += 1
return orig(g, xx, ww, masks, sg, sw, sx, fmt)
return orig(*args, **kwargs)
f8mod.linear_backward_fp8 = spy
try:
@@ -331,14 +376,20 @@ def test_fp8_autocast_context():
def test_fp8_tensor_meta_delayed_update():
"""Meta seeds from data and refreshes the scale from the amax ring."""
"""Meta seeds from data; hist/scale are packed views of one state buffer."""
meta = FP8TensorMeta(torch.device("cpu"), DelayedScaling(history_len=4, margin=0))
w = torch.randn(8, 8)
meta.w.seed(w, "e4m3")
assert meta.w.initialized
torch.testing.assert_close(meta.w.scale, (w.abs().amax() / 448.0).reshape(1))
meta.w.update(torch.tensor([4.0]), "e4m3")
torch.testing.assert_close(meta.w.scale, torch.tensor(4.0 / 448.0).reshape(1))
# [hist | scale | counter] packing: views alias the single state buffer.
assert meta.w.state.numel() == 4 + 2
assert meta.w.hist.data_ptr() == meta.w.state.data_ptr()
assert meta.w.scale.data_ptr() == meta.w.state[4:].data_ptr()
# counter slot stays int32-zero (float bits) between launches
assert meta.w.state[-1].view(torch.int32).item() == 0
meta.w.advance()
assert meta.w.idx == 1
def test_quantize_bf16_cpu_fallback():