Trie packing
Compute shared prefixes once. Preserve the original objective. Let ordinary backpropagation accumulate the gradients.
Three sequences, one shared tree
A trie is a prefix tree. Trie packing merges identical prefixes across training sequences into a single set of token nodes. Each root-to-leaf path still represents an original sequence. A token is shareable only when its entire preceding context is identical.
Before: separate sequences
Sequence 1: A B C D
Sequence 2: A B C E
Sequence 3: A B F G12 token occurrences.
A and B occur three times; C occurs twice.
Letters stand for token IDs. We use next-token cross-entropy, with no BOS/EOS, padding, dropout, or masked targets in this example.
After: unique prefix nodes
7 unique nodes. Highlighted nodes have multiple trajectory occurrences.
One packed tensor, three logical sequences. Ordinary sequence packing removes padding; trie packing also removes repeated prefix computation. It optimizes the training pass and does not require changing how rollouts are generated.
The formulas stay; the layout and visibility change
Step 1 · Store each node once
packed tokens: [A, B, C, D, E, F, G]
packed index: 0 1 2 3 4 5 6
position IDs: [0, 1, 2, 3, 3, 2, 3]
input_ids: [1, 7]
hidden: [1, 7, hidden_dim]
Position IDs use the node’s depth on its original path, not its flattened index. F is at packed index 5 but position 2 in ABFG. Use these positions for RoPE or learned position embeddings.
Step 2 · Allow only ancestors and self
Apply this visibility at every attention layer. Rows are queries; columns are keys. A 1 means visible.
| Q / K | A | B | C | D | E | F | G |
|---|---|---|---|---|---|---|---|
| A | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| B | 1 | 1 | 0 | 0 | 0 | 0 | 0 |
| C | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
| D | 1 | 1 | 1 | 1 | 0 | 0 | 0 |
| E | 1 | 1 | 1 | 0 | 1 | 0 | 0 |
| F | 1 | 1 | 0 | 0 | 0 | 1 | 0 |
| G | 1 | 1 | 0 | 0 | 0 | 1 | 1 |
E sees ABCE, never D. G sees ABFG, never CDE. This is not the ordinary lower-triangular mask of a length-7 sequence.
Step 3 · Run the ordinary transformer operations
Mtree is 0 for allowed connections and −∞ elsewhere. Q/K/V projections, FFNs, residuals, and token-wise normalization use their usual formulas. The FFN processes 7 rows rather than 12.
Why this works: identical initial representations plus identical visible contexts produce identical layer outputs. Repeating this argument across layers makes every packed node equivalent to its unpacked occurrences, under deterministic, compatible model behavior.
Share logits, preserve every prediction
At node u, the LM head produces logits zu = huWLM: one raw score per vocabulary token. Softmax gives the next-token distribution pu. The logits after C predict D or E; they do not predict C itself.
| Original sequence | Predictions | Packed logit rows | Targets |
|---|---|---|---|
| ABCD | A → B, B → C, C → D | 0, 1, 2 | B, C, D |
| ABCE | A → B, B → C, C → E | 0, 1, 2 | B, C, E |
| ABFG | A → B, B → F, F → G | 0, 1, 5 | B, F, G |
This correspondence is the loss mapping. It preserves nine prediction occurrences using four distinct logit rows.
Keep the original denominator. Averaging over seven unique nodes changes this objective. D, E, and G have no next-token targets here because EOS is omitted. Their activation rows may be omitted in an optimized implementation; we retain them to make the tree easy to inspect.
Same chain rule, shared computation graph
Below, GX means ∂L/∂X. We show derivatives for Lsum; divide them by 9 for Lmean.
Step 1 · Combine gradients at the shared logits
For target y, ℓ = −log p(y) = −zy + log Σj exp(zj). Differentiating gives Gz = p − ey, where ey is the target’s one-hot vector.
At C, the gradients for targets D and E add: (pC − eD) + (pC − eE). Different target contributions are preserved; they are not replaced with an arbitrary branch’s gradient.
Step 2 · Run the usual LM-head and FFN backward
The shared row’s incoming gradient is already accumulated. FFNs and normalization need no tree-specific adjustment. Residual branches add their gradients as usual.
For any shared linear operation Y = XW, two incoming contributions G1 and G2 can be combined before the matrix multiplications:
This principle also holds for nonlinear layers: at fixed forward activations, the backward Jacobian-vector operation is linear in its incoming gradient.
Step 3 · Attention collects descendant contributions
From O = PV, ordinary matrix backward gives:
For shared value row B, the final attention layer receives:
Queries B, C, and F all use the same VB, so their gradients add. Queries D, E, and G contribute zero in this no-EOS example because their output activations do not affect any loss. With EOS targets, their contributions would be included too.
Through softmax and the QK product, for score matrix S = QKT/√dk + Mtree:
Masked edges contribute zero. These are single-head equations before any RoPE transformation; a full block also backpropagates through RoPE, output projection, normalization, and residual paths normally.
Step 4 · Repeat through earlier layers
The accumulated input gradient flows into the preceding layer. Repeat ordinary backward until reaching the embeddings. A loss-masked prompt node can still receive gradients from descendants that attend to it.
No extra branch-count multiplier. Correct loss mapping and attention backward already perform the required accumulation. Multiplying every shared activation gradient again would generally double-count it.
The training-loop change is small
This PyTorch sketch assumes a model adapter that accepts the tree mask with the correct backend-specific shape and convention. It is not a drop-in configuration for every model.
import torch
import torch.nn.functional as F
# A ... G are vocabulary token IDs, not packed row indices.
ids = torch.tensor([[A, B, C, D, E, F, G]], device=device)
positions = torch.tensor([[0, 1, 2, 3, 3, 2, 3]], device=device)
outputs = model(
input_ids=ids,
position_ids=positions,
attention_mask=tree_mask, # Correctly adapted for this backend
)
logits = outputs.logits[0] # [7, vocab_size]; select batch item 0
# One entry per ORIGINAL prediction occurrence.
source_rows = torch.tensor(
[0, 1, 2, 0, 1, 2, 0, 1, 5], device=device
)
targets = torch.tensor(
[B, C, D, B, C, E, B, F, G], device=device
)
log_probs = F.log_softmax(logits, dim=-1)
token_logps = log_probs[source_rows, targets] # [9]
loss = -token_logps.mean() # Original denominator
optimizer.zero_grad(set_to_none=True)
loss.backward() # Unchanged
optimizer.step() # Unchanged
Why indexing matters: differentiable indexing scatters and adds gradients into repeated source entries during backward. It preserves repeated targets without requiring custom hooks. Gathering target log-probabilities also avoids constructing nine full vocabulary-sized logit rows.
For PPO/GRPO, gather the same per-occurrence log-probabilities, then apply each occurrence’s own advantage, old log-probability, clipping rule, mask, and weight. Do not generally average advantages before applying a nonlinear clipped loss.
What changes, what does not
| Component | Required behavior |
|---|---|
| Data collator | Build trie, flatten nodes, retain original occurrence-to-node mappings. |
| Positions and attention | Use path positions and correct visibility at every layer. |
| Loss | Preserve original targets, masks, weights, and normalization. |
| Autograd / token-wise layers | No special hooks or branch-count multipliers. |
| Custom attention backward | Correctly accumulate shared dK/dV across query tiles; never overwrite competing contributions. |
| Optimizer | Ordinary update with equivalent accumulated parameter gradients. |
Memory: a dense mask is not enough
In this example, dense scores grow from 3 × 4² = 48 entries per head to 7² = 49, even though allowed query–key pairs fall from 30 to 21. Larger trees can amplify the dense-matrix overhead.
Use a tree-capable tiled attention implementation to avoid storing the full score matrix, and block sparsity to skip forbidden work. Compact mask construction and a unique-token budget per pack also matter. Peak memory can still exceed a baseline that processes one trajectory at a time.
Verify equivalence before measuring speed
- Start with the same parameters and deterministic forward behavior. Disable dropout for the first comparison.
- Compare every mapped packed logit row with its corresponding unpacked row.
- Compare the loss with identical reduction, masking, and sample weighting.
- Compare every parameter gradient, then one optimizer update, within dtype-appropriate numerical tolerances.
- Test forks, repeated targets, EOS, padding, masked prompts, unequal sequence lengths, and multiple packed trees.
Watch the assumptions: detached prefix K/V breaks gradient flow. Independent dropout masks prevent exact sample-wise identity. MoE capacity limits and auxiliary losses may depend on original token multiplicities. Relative/windowed attention must preserve the original path semantics. Distributed loss scaling and checkpoint recomputation must remain consistent.
The durable mental model: preserve the forward semantics and the original objective, then apply ordinary backpropagation to the shared graph. The derivative rules are unchanged; duplicated computation is removed.
Implementation references
The walkthrough above is a mathematical reference design. Framework APIs and optimized kernels differ.
- Tree Training: Accelerating Agentic LLMs Training via Shared Prefix Reuse — tree packing and gradient restoration.
- AReaL: Tree Training — configuration and backend support.
- TRL: experimental asynchronous GRPO — prefix-tree layout and per-occurrence loss mapping.
- PyTorch: FlexAttention — custom attention masks and block sparsity.