The Short Story
Last week the Cat Chat GPU training path was nine out of eleven stages green and a queue of numerical warnings. This week it is eleven out of eleven, ./scripts/test.sh --tools passes 268/268, and the current training run completed 1066 steps with a final running loss of 3.55 and an eval top-5 of 27.09%. The fixes are small, but the explanation matters: FP16 BPTT is a precision contract, and every backprop step is a place for that contract to break.
What Went Wrong in FP16 BPTT
Three issues surfaced as soon as the model left the short benchmark runs and tried to train on real sequences. First, the per-timestep hidden-state gradient grad_h in the GRU backward pass would saturate in FP16, not because the values were large in absolute terms, but because the clamp was being interpreted as already scaled. With loss_scale=1 the value looked reasonable, but the same config with a static loss scale would have silently changed the effective clamp. We moved the multiplication into nn/systems/lm/training/gpu/gpu_trainer_dispatch.cpp, so the user-facing grad_h_clamp is always an unscaled value and the dispatch scales it by loss_scale before it reaches the shader.
Second, the RMSNorm shader computed inv_rms = 1 / sqrt(mean_sq + eps). For a nearly-zero hidden state at the start of training, mean_sq + eps is still small and inv_rms can explode into the tens of thousands, feeding non-finite values straight back into BPTT. We added min(1.0 / rms, 1000.0) and a mean_sq <= 1e-6 identity fallback in assets/shaders/cat_chat/rmsnorm.comp to match the CPU reference and stop the divide-by-near-zero.
Third, Stage 6/9 of the cat_chat --gpu-test ladder compares GPU Tensor Core cooperative-matrix accumulation against the CPU reference. The accumulation order is non-deterministic across subgroups, so an absolute tolerance would fail even when the result was mathematically correct. We switched to a relative tolerance for the cooperative-matrix accumulation-order non-determinism and lowered the gradient-precision test from a higher static scale to loss_scale=128, which still exercises the precision path without masking real errors.
grad_h clamp
Loss-scale-aware dispatch in gpu_trainer_dispatch.cpp keeps grad_h_clamp invariant under different FP16 loss-scale choices.
RMSNorm inv_rms
A clamp at 1000.0 and a near-zero identity guard in the shader prevent 1/√mean_sq blow-up for small hidden states.
Stage 6/9 tolerance
Relative tolerance for cooperative-matrix non-determinism plus loss_scale=128 for the gradient-precision test.
Test coverage
cat_chat --gpu-test now 11/11 and ./scripts/test.sh --tools 268/268.
Training Telemetry: Previous vs. Current
The previous run is the longest available historical telemetry captured before the current fixes; the current run is the one taken after them. The old curve is short because the pipeline was not yet stable enough to run for thousands of steps. The new curve is the first long run under the production config. Direct step-for-step comparison is approximate because vocabulary size, sequence length, and per-step diagnostics differ between the two runs.
Previous run: 17 steps, final running loss 7.79, top-1 3.35%, top-5 14.02%. Current run: 1066 steps, final running loss 3.55, top-1 5.10%, top-5 27.09%. Source: data/cat_chat_gpu/training_telemetry_history_20260802_033754.csv (previous) and data/cat_chat/training_telemetry.csv (current).
What 11/11 Means
The cat_chat --gpu-test ladder is an incremental check of the GPU training pipeline: GEMM, GRU gates, RMSNorm, attention, softmax cross-entropy, gradient reduction, and optimizer. Each stage compares the GPU against the CPU reference on the same batch. Moving from 9/11 to 11/11 does not mean the numerical problems are gone forever; it means every known class of failure has a guard and a test, so the next failure will be a new one instead of a repeat.
./scripts/test.sh --tools passing 268/268 gives the same guarantee at the tool boundary. The CPU and GPU paths are still different machines running different code, but the contracts they agree on are now encoded in tests rather than hope.
vNext: Dual-Stream Attention Decoder V3
With the FP16 BPTT pipeline stable, the next run is CAT VLM V3.5, also called the Dual-Stream Attention Decoder V3. It keeps all the fixes above, then extends them:
Larger context
max_seq_len grows from 256 to 512 tokens. The context stream is the whole reason the dual-stream architecture exists; giving it twice the window should let it catch longer-range dependencies.
Longer training run
The current run proves the pipeline can survive 1,000+ steps. The next run will train longer and log per-step eval and gradient-norm telemetry now that the pipeline is trustworthy.
Production config
grad_h_clamp=1000.0 with loss_scale=1.0, dispatched as an unscaled clamp multiplied by loss scale at the shader push constants.
Richer telemetry
Per-step eval accuracy and gradient norms will be written to the telemetry CSV at regular intervals, not only at the final step.
Related Work
These posts are the foundation this one builds on:
