Skip to content

fix(ricursive): mask from actual cache positions, keep labeled loss - #3551

Merged
Qubitium merged 1 commit into
ModelCloud:mainfrom
Qubitium:fix/ricursive-staticcache-labels
Nov 6, 2026
Merged

Qubitium merged 1 commit into
ModelCloud:mainfrom
Qubitium:fix/ricursive-staticcache-labels

Conversation

@Qubitium

@Qubitium Qubitium commented Nov 6, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #3545 and #3549 (both merged). Two defects found in review of the mirrored HF patch.

1. Preallocated caches broke causal attention

StaticCache layers return their full preallocated capacity from update(), so k.shape[2] is capacity, not occupied context. A 6-token prefill into a 16-slot cache was therefore treated as a 10-token prefix: the end-anchored verify mask gave query positions 10-15, letting each query attend slots of a different absolute position. Changing token 6 changed token 1's output (measured max diff 0.4830), which is impossible under causal attention.

Fix: read the layer's real length via Cache.get_seq_length(layer_idx), trim the unwritten tail of k/v, build the mask from cache_position and the absolute key positions (trailing window for evicting caches), and align an incoming mask that is longer than the keys actually present.

2. labels + logits_to_keep=1 returned NaN loss

The patch trimmed hidden states and labels to one position, then the causal shift produced empty tensors and cross_entropy returned NaN.

Fix: when labels are present, keep the full sequence so the shifted loss is defined, and trim only the returned logits. Unlabeled calls still slice the LM-head input, so the [batch, seq, vocab] projection is never materialised during generation.

Validation

  • tests/test_ricursive_support.py gains test_static_cache_uses_actual_positions_not_capacity and test_labels_with_logits_to_keep_keeps_finite_loss; 7 focused tests pass on CPU.
  • Tiny-fixture GPU run (original upstream modeling file, so these patches are exercised): StaticCache causality diff 0.0 (was 0.4830), StaticCache warm block 0.0, labels+logits_to_keep=1 loss 5.562519 (full 5.562519, finite), logits_to_keep=1 shape (1,1,256) and max diff 8.9e-08, cached == uncached generation, bf16 causality 0.0, peak test allocation 9.4 MiB.

StaticCache returns its full preallocated capacity from update(), so using
k.shape[2] as the occupied length gave a 6-token prompt in a 16-slot cache
query positions 10-15; changing a later token then changed earlier outputs.
Trim the unwritten tail and build the verify mask from the layer's actual
get_seq_length() and cache_position, aligning an oversized incoming mask.

logits_to_keep=1 with labels trimmed both logits and labels to one position,
leaving the causal shift empty and the loss NaN. Keep the full sequence when
labels are present so the loss is defined, and trim only the returned logits.
@Qubitium

Qubitium commented Nov 6, 2026

Copy link
Copy Markdown
Collaborator Author

Full-model verification of this branch: Ripple-65B-Preview GPTQ W4A16 (Marlin), single RTX 4090, bf16, 40.2 GiB peak, using the native modeling_ricursive.py plus these load-time patches.

Scenario Result
StaticCache causality: 6-token prefill into a 16-slot cache, change token 6 max diff on tokens 1-5 = 0.0
Warm verify block, DynamicCache, N=48 + M=8 vs one 56-token forward max abs 0.25 (rel 1.4e-2)
Warm verify block, StaticCache 64 slots max abs 0.25
Control: single-token decode step vs same full-forward row (no block mask involved) max abs 0.156 (bf16/Marlin shape-noise floor)
Pre-fix mask emulation (top-left SDPA causal) max abs 32.31 (rel 1.76), argmax correct 0/8
labels + logits_to_keep=1 loss 16.5 = full 16.5, logits (1,1,100352)
logits_to_keep=1 (1,1,100352), argmax matches full last row
cached vs uncached greedy generate identical
Chunked MLP, RIPPLE_MLP_CHUNK=64 vs 0 torch.equal=True
DFlash2 dflash_generate, block_size=1 vs AR 0 divergences
DFlash2 default block vs AR first divergence at token 1 is an exact bf16 tie (top-2 both 18.0, gap 0.0); accept_mean 2.56

The block-path error (0.25) is ~1.4x the no-mask control (0.156) and ~130x smaller than the pre-fix 32.31, i.e. the verify mask is correct and the residual is bf16 kernel shape noise. The DFlash token mismatch is a genuine tie at bf16 precision, not a masking error.

Re-ran the same suite with apply_ricursive_generate_patch disabled (patches all report False), so only the native HF file is active: all pass with the same numbers.

@Qubitium
Qubitium merged commit e5547e1 into ModelCloud:main Nov 6, 2026
1 check passed
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