Repository navigation
Sum-layer precision option, and dense sum layers through cuBLAS - #62
Merged
Merged
Conversation
…se_bf16 / force_use_fp32 TensorCircuit.precision (and juice.compile(..., precision = ...)) sets the precision of the sum layers' products in the forward and the backward pass: * "auto" (the default) keeps today's kernels: bf16 products in the block-sparse kernels from batch 16, about 4e-3 in a log-likelihood; * "tf32" never uses bf16: round-to-nearest TF32 products, about 5e-4; * "fp32" gives fp32-level products in the forward (tf32x3 in the Triton tlmm kernel, about 1e-6) and computes the flows without tensor cores in the backward, as force_use_fp32 did. Batches below 16 run fp32 kernels in every mode, as before. The mode can be changed at any time. It reaches every sum layer in the forward, the backward and the partition-gradient helper, and a recorded CUDA graph is keyed on it. force_use_bf16 and force_use_fp32 are removed. Passed as False (their old default) they are ignored; passed as True they raise a TypeError naming the replacement, instead of computing in a precision the caller did not ask for. The tests that set force_use_fp32 now set pc.precision. The tlmm kernel gets a constexpr fp32_dot (default False). It is passed only in "fp32" mode, so "auto" and "tf32" launch the kernel exactly as before. New tests: tests/layer/sum_layer_precision_test.py (each mode against a float64 forward, the removed flags at the layer, an unknown mode) and tests/model/precision_option_test.py (compiling with and setting the mode, every sum layer receiving it in both passes, the removed flags at the circuit, CUDA graphs keyed on it).
A sum layer whose node blocks are dense -- children a contiguous run of element rows, parameters strided by the block size -- is one matrix product per block. For LL without tempering, at block size >= 1024, batch >= 16 and children x batch >= 2^19, such a layer now exponentiates its children, multiplies on cuBLAS using views of the parameters (no copy) and takes the log. Fused Triton kernels compute the column maximum, the exp and the log (layer/kernels/sum_forward_dense.py). The products are TF32 in "auto" and "tf32" and exact fp32 in "fp32". The gate is fixed and never chosen by timing, because the two paths differ numerically. Node blocks that share their children (an HMM layer) exponentiate them once for all blocks. Blocks that each have their own children, evenly strided in children, node rows and parameters (HCLT: 2-127 groups per layer), run as batched GEMMs over [groups, ...] views, with as many groups per GEMM as a 64 MB scratch buffer holds. Any other layout keeps the kernels. Looping over HCLT-1024's 452 groups one at a time ran its forward at 0.71x the kernels' speed. Whole-circuit forward / forward+backward against the parent commit, "auto", on an RTX PRO 6000 (median over processes): HMM, 4096 latents, 32 positions, batch 128 x2.25 / x1.31 HMM, 4096 latents, 32 positions, batch 512 x1.57 / x1.12 HMM, 1024 latents, 64 positions, batch 512 x1.10 / x1.02 HCLT, 1024 latents, 768 variables, batch 512 x1.20 / x1.05 Where the gate takes no layer (smaller batches, PD, HMM 1024 up to batch 256), times are unchanged within about 1%. The dense layers also get TF32 instead of bf16 products: on a 1024-latent HMM layer at batch 128 the log-likelihood error drops from about 4e-3 to 5e-4. On HCLT at batch 512 the path needs 64 MB of scratch, against 3 MB for the kernels, with 6.7 GB resident. New tests in tests/layer/sum_layer_precision_test.py: * each mode against a float64 forward, on a one-group layer and on a three-group batched layer; * per-group column maxima: a -inf group, a group 200 below the others, and a maximum in a later row tile; * batched runs of one and two groups; * the gate; * the plans _dense_blocks makes of made-up layouts; * block-sparse layers staying on the kernels. Each of nine mutations of the new code is caught.
In "fp32" mode the element- and parameter-flow kernels dropped tl.dot for broadcast sums on CUDA cores, as force_use_fp32 always did. The backward of a 4096-latent HMM at batch 512 took 1.6 s, against 32 ms in "auto". The kernels now keep the dots and run them in three TF32 passes (input_precision = "tf32x3"): fp32-level, on tensor cores. On a 64-latent HMM layer at batch 48 the flows are 5e-7 from float64, relative to the largest flow. With the round-to-nearest TF32 dots of "auto" and "tf32" they are 5e-5 (element flows) and 3e-4 (parameter flows). The kernels take a constexpr DOT_TF32X3 (default False), passed only in "fp32" mode, so "auto" and "tf32" launch them as before. The autotune keys include it. PYJUICE_PAR_DOT_IEEE still takes precedence in the parameter flows. The CUDA element- and parameter-flow kernels (fp16 / TF32 dots) stay off in "fp32" mode, and tiles below 16 still sum without dots. Whole-circuit forward + backward in "fp32", before / after: HMM, 4096 latents, 32 positions, batch 512 1608.9 / 49.9 ms HMM, 4096 latents, 32 positions, batch 128 299.6 / 17.9 ms HMM, 1024 latents, 64 positions, batch 512 153.1 / 12.3 ms HCLT, 1024 latents, 768 variables, batch 512 1579.4 / 64.3 ms PD, 256 latents, batch 512 105.2 / 64.2 ms "fp32" now runs at 0.64-0.98x the speed of "auto"; before this change it was 2-50x slower. New test: one layer's element and parameter flows against float64 in each mode, with "fp32" passing tf32x3 to both flow kernels and the other modes passing nothing. Downgrading either kernel's tf32x3 dots to TF32 fails it.
… is measured once per launch shape and shared process-wide -- two identical models no longer take different arms and differ by ~1e-6
SumLayer's CUDA (CuTe) tlmm forward fast path timed Triton against its valid CUDA tile configs per layer and kept
the winner on the layer (`_cached_fw_cuda_choice`). The arms are numerically equivalent but not bit-identical
(~1e-6 apart), and with the winner decided by a timing close to the margin, two structurally identical models
could take different arms. precision_option_test's test_recorded_graphs_are_keyed_on_it (a CUDA-graph HMM against
its eager twin, bit for bit) failed in every full-suite run, 3 of 3 on croissant and brezel, while passing when
run alone: the eager twin's first sum layer on Triton, the graph model's on CUDA config 1, 9.5e-7 apart.
The choice now goes through `autotune.choose`, new beside `pick`: measured once per key by `best_of` (Triton the
reference, kept unless a CUDA config clears MARGIN, as before) and cached in the autotuner's process-wide cache,
under ("sum_fw_tlmm_cuda", num_edges) + the forward's shape key, as the small-batch gate's choice already is.
Unlike `pick` it measures whatever ENABLED says, as this gate always has; during a CUDA graph capture an
unmeasured shape takes Triton, uncached. What changes is only which layers share a measurement: identical shapes
share one, and a model measures each shape once instead of each layer. No kernel changes.
Tests: sum_layer_tlmm_choice_test -- with a measurement that names a different arm every time it is asked, the
second of two identical HMMs measures nothing and matches the first bit for bit; it fails without this fix.
The full suite passes on croissant with four workers on one GPU, where both failed the CUDA-graph test before:
1298 tests on this branch, and 1298 merged into main.
liuanji
added a commit
that referenced
this pull request
Oct 22, 2026
Brings in #62: the sum layers' precision option, the cuBLAS dense path, the tf32x3 backward, and the shared choice of the tlmm fast path. #63, the z-correction default, is on this branch already as 1ed9d48. Conflicts: tensorcircuit.py and block_scale.py, both split into modules on this branch. Both take the branch's side. tensorcircuit.py adds #62's import of PRECISIONS and _pop_removed_precision_flags, and #62's change to the element-flow helper goes to model/inspection.py, where that helper now lives. block_scale.py's split modules already carry #63.
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.
Summary
1. A precision option for the sum layers (
ebc503b3).TensorCircuit.precision(alsojuice.compile(..., precision = ...)) replacesforce_use_bf16/force_use_fp32:"auto"(default)"tf32""fp32"tf32x3in the Triton tlmm kernel)tf32x3)The mode can be changed at any time, and a recorded CUDA graph is keyed on it. Batches below 16 run fp32 kernels in every mode, as before.
2. Dense sum layers through cuBLAS (
79b76e7e). A large dense layer exponentiates its children, multiplies on cuBLAS using views of the parameters, and takes the log. "Large" means block size ≥ 1024 and children × batch ≥ 2^19, for LL without tempering.The products are TF32 in
"auto"and"tf32", and exact fp32 in"fp32". The gate is fixed, never chosen by timing, because the two paths differ numerically.3.
"fp32"'s backward on tensor cores (18f7192f). In"fp32"mode the element- and parameter-flow kernels used to droptl.dotfor broadcast sums on CUDA cores, asforce_use_fp32always did. They now keep the dots and run them in three TF32 passes (input_precision = "tf32x3")."auto"and"tf32"is 5e-5 (element flows) and 3e-4 (parameter flows)."fp32".Breaking change
force_use_bf16 = True/force_use_fp32 = Truenow raise aTypeErrorthat names the replacement, inTensorCircuit.forward/backwardand inSumLayer.forward/backward. PassingFalse(their old default) is still accepted and ignored.Results
Whole-circuit timings against
main,"auto", RTX PRO 6000. Each figure is the median over processes: two GPUs on different CPU sockets with the arms swapped, plus alternating one-at-a-time runs.Accuracy: dense layers now use TF32 instead of bf16 products. On a 1024-latent HMM layer at batch 128, the log-likelihood error drops from ~4e-3 to ~5e-4.
Memory: on HCLT at batch 512 the dense path needs 64 MB of scratch, against 3 MB for the kernels, with 6.7 GB resident.
"fp32"speed: forward+backward is now ×0.64–0.98 of"auto", from 2–50× slower before the third commit:"auto""fp32", broadcast-sum backward"fp32",tf32x3backwardTests
Full suite: 1301 passed, 26 skipped.
tests/layer/sum_layer_precision_test.py:"fp32"passingtf32x3to both flow kernels and the other modes passing nothing. Downgrading either kernel's dots to TF32 fails it;tests/model/precision_option_test.py:force_use_fp32now setpc.precision.