Skip to content

Layer streaming: a checkpoint-visible fused NF4 dequantise-and-multiply (upper bound 9.8% of the step) #842

Open

Description

@MakazhanAlpamys

Why

benchmarks/probe-v0.73.0-what-bounds-streaming.md ablated the streamed Llama-3.1-8B NF4 step on the RTX 3050. The setup was batch 1, S=512, interleaved arms, all at 960 MHz. Removing the NF4 dequantisation took the step from 4.211 s to 3.799 s, -9.80% (lines 155-164). Removing every host-to-device transfer bought 1.44%. The mechanism is that every streamed weight is dequantised into a dense bf16 tensor on every visit: 448 calls per step, about 437 MB of VRAM writes per layer visit (lines 299-304).

9.8% is a ceiling, not a forecast. The ablation arm multiplied by a cached zero weight and produced garbage mathematics (lines 151-153). The record says outright that it did not measure what a real fused kernel would cost (lines 540-542). If a kernel appears to beat 9.8%, the two arms differ in something other than the dequantisation.

The constraint: #331

The dequantisation is there on purpose. MatMul4Bit keeps the packed weight and quant_state on ctx as plain attributes, not through save_for_backward (src/soup_cli/utils/layer_stream_runtime.py:164-168). torch.utils.checkpoint cannot see them, so the reference aliases the buffer pool. Above roughly 165 MiB per layer the gradients came back wrong while the forward and the loss stayed bit-exact (#331).

De-aliasing was measured and rejected: 4 220 -> 19 720 MiB on 32B (lines 177-179). The shipped repair is install_dequant_forward (lines 161-230). It dequantises inside the checkpointed region and calls F.linear, so the dense weight is saved through the ordinary mechanism and recomputed (lines 181-184, 220-223). On real 32B the repair cost +650.9 MiB (2.71 layer-equivalents) and -4.8% throughput (benchmarks/gate-h100-validation.md:2674-2682).

So the requirement is not simply "fuse it". It is: fuse it without putting the packed weight back where checkpointing cannot see it.

A nuance the two records state differently

The probe record calls the dequantisation the only streaming-specific cost (line 304). The H100 record shows that at real training shapes bitsandbytes itself already takes _dequant_linear_fallback for resident NF4. Its fused gemm_4bit runs only at small M, at most 32 on the architectures queried (gate-h100-validation.md:2601-2620; also layer_stream_runtime.py:186-189).

The consequence: a large-M fused NF4 kernel is something neither path has today. It would speed up resident NF4 training at those shapes too, and whether resident adopts it decides what "bit-exact vs resident" means (see Acceptance criteria).

Scope

In: the forward and the input-gradient of streamed Linear4bit projections under NF4 streaming.

Out: bf16 streaming (it has no dequantisation), the per-visit Python and launch overhead (a separate item, and the cheaper one), host-to-device bandwidth (1.4%), and a weight gradient (the base is frozen).

Design sketch

  • Before writing a kernel, check whether an existing library already ships a large-M NF4 GEMM that meets the constraints below.

  • A custom autograd Function (or torch custom op) at the plug point in _dequant_forward (layer_stream_runtime.py:220-223). Keep that function's dtype handling, which mirrors Linear4bit.forward line for line (lines 186-192).

  • Save only through save_for_backward: the packed nibbles, the absmax and the nested statistics. No tensor may be held on ctx as an attribute. That is the whole Layer streaming: NF4 above ~165 MiB/layer produces silently wrong gradients (forward stays bit-exact) #331 lesson.

  • The backward computes only grad_x = grad_y @ W with a transposed fused kernel. No dense W is ever materialised or saved.

  • Deterministic reduction. No atomics and no non-deterministic split-K. The project's gate is equality, and a kernel that is not reproducible against itself destroys it. The same record measured that for Cut Cross-Entropy (lines 404-419).

  • Possible side effect, to measure rather than claim: F.linear currently saves one layer's dense weights across the recompute. Not saving them may recover part of the repair's +650.9 MiB.

Acceptance criteria

Upstream

bitsandbytes#2034 (MatMul4Bit keeps B and quant_state on ctx) is open as of this writing. Close or re-scope this issue if upstream fixes it in a way that survives checkpointing and gives a training-M path that does not materialise a dense weight. A #2034 fix on its own would let streamed weights back through bitsandbytes, but at training M that still means _dequant_linear_fallback, which costs the same as today.

Hardware needed

A CUDA GPU. The RTX 5070 Laptop (8 GB, sm_120) is enough for the aliasing control and for 8B NF4 streaming, whose peak was 3.448 GB on the 3050 (probe record line 103). The kernel toolchain must target sm_120. On Windows, Triton ships as the triton_windows distribution, and that name already broke a version check once (probe record line 331). All numbers above are from the RTX 3050 and an 8x H100 box. A 5070 measurement is a new baseline.

Honest limits


Related, filed the same day:

Activity

  1. MakazhanAlpamys commented on Sep 16, 2026

    @MakazhanAlpamys
    OwnerAuthor

    Same measurement (benchmarks/probe-rtx5070-what-bounds-streaming.md §21, #1028; the ablation arms of that record's §4, interleaved, two rounds, on the post-#974 reader; synthetic 70B NF4, disk tier, batch 1, seq 512 and 256):

    arm seq 512 seq 256
    A baseline 17.14 / 17.21 s 17.00 / 17.01 s
    B no read, no copy 6.45 / 6.40 s 3.82 / 3.85 s
    C no dequantisation 17.14 / 16.99 s 17.05 / 17.00 s
    D neither 4.97 / 4.97 s 2.55 / 2.52 s
    • The dequantisation costs 1.30–1.46 s per step, 8–9 ms per layer visit, token-independent (B − D at both shapes) — 23% of the compute floor, the figure this issue was opened on (RAM tier, §4 of the same record). It also absorbs ~8 of Layer streaming: cut the ~10 ms fixed cost per layer visit, which dominates the step below ~128 tokens #841's ~10 ms per visit; the pure fixed cost is 0.65 ms per visit (arm D from the two shapes).
    • On the cold disk tier it is invisible: C equals A at both shapes. The step is read-bound outright (A does not respond to halving the tokens), so a fused kernel buys nothing there until the read is faster than the compute — which the two-point fit puts at ~1,560 tokens per step on this fixture, or after the reader closes its 4.1 → 5.65 GB/s gap.
    • On the RAM tier it is the lever, with the ceiling the plan already states: at most the 23% measured, half of that a success. The constraint is unchanged — fuse without putting the packed weight where torch.utils.checkpoint cannot see it, gated bit-exact forward AND backward against resident NF4, with the repair-disabled control in the same process.

    So: this is the item to build for the RAM tier, with #841 folded in; it is not the item that moves a 70B on a laptop, and its record should say so up front rather than let a 23% headline travel to the tier where it is 0%.

  2. added a commit that references this issue on Sep 18, 2026
  3. Amix29 commented on Sep 21, 2026

    @Amix29
    Contributor

    I’ll take #842, with #841 folded in per the maintainer’s 2026-09-16 recommendation. I’ll target the actual dominant fixed cost: replace the checkpoint-safe explicit dequantize_4bit + F.linear path with a checkpoint-visible 4-bit matmul path that does not retain an aliased pooled packed buffer across recompute. I’ll first prove the autograd/checkpoint lifetime on CPU/unit-level structure, then keep the PR draft until the required CUDA correctness/performance gates are run. Any residual #841 Python overhead will be measured separately, not conflated with the kernel win.

  4. Amix29 commented on Sep 21, 2026

    @Amix29
    Contributor

    Draft #1143 now carries the checkpoint-visible fused-forward implementation plus the folded #841 substitution-view cache. Local validation: 132 targeted tests passed, 7 skipped; the recycled-buffer checkpoint control recomputes and matches the private gradient exactly, while the plain-ctx negative control reproduces the stale-gradient bug. The remaining blocker is explicit: bitsandbytes has no transposed 4-bit GEMM for grad_x, so backward still dequantizes and the CUDA performance gates remain open.

  5. MakazhanAlpamys commented on Sep 24, 2026

    @MakazhanAlpamys
    OwnerAuthor

    Recording a ruling from #1143 here, because it changes this issue's acceptance criteria. The reference for streamed NF4 is native bitsandbytes gemm_4bit (#1143 (comment)). Streamed NF4 must match resident NF4 bit for bit on any card, the RTX 5070 (sm_120) included, so the #776 caveat in the criteria above no longer applies. What #1143 needs before it can merge is listed in that comment.

  6. added
    claimedSomeone is actively working on this — check the thread before starting
    and removed
    help wantedExtra attention is needed
    on Sep 24, 2026
  7. MakazhanAlpamys commented on Sep 29, 2026

    @MakazhanAlpamys
    OwnerAuthor

    #1143 (merged as 33639fb4), by @Amix29, lands the code part of this issue. Thank you.
    Streamed NF4 on CUDA now follows bitsandbytes' own fused-versus-dequantise dispatch and runs the fused GEMM through a checkpoint-visible Function, so it matches resident NF4 bit for bit on the RTX 5070 (sm_120) as well. The issue stays open for its measurement criteria: the aliasing control above ~165 MiB per layer, streamed vs resident stated separately for forward and backward, kernel vs dequantise per shape and M, and tok/s with peak VRAM against a same-session GEMM ceiling. Try: soup train --config soup.yaml with training.stream_layers: true and training.quantization: 4bit.

  8. added
    help wantedExtra attention is needed
    and removed
    claimedSomeone is actively working on this — check the thread before starting
    on Oct 4, 2026
  9. MakazhanAlpamys commented on Oct 4, 2026

    @MakazhanAlpamys
    OwnerAuthor

    Following @Amix29's answer on #837: claimed is removed and help wanted is restored here as well. What is left is the CUDA-only measurement, open to anyone with a card; the credit for the merged work stays with @Amix29.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:trainingReLoRA, preference training and layer streamingdifficulty:9Difficulty 9/10: new subsystem or special hardware.enhancementNew feature or requesthelp wantedExtra attention is needed

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions