← All experiments THE RESEARCH RECORD / tanh-recurrence

Tanh recurrent AGPT

Can AGPT's prefix-trie aggregation train a non-attention f_theta, specifically a simple tanh recurrence over prefix transitions?

experimentconcludedpositiverecurrenteval: legacy
Opened Updated
THE ANSWER SO FAR

Yes. Plain tanh at pd=1 trains cleanly to held-out PPL 6.31 (depth 8, d_model 64, epoch 500). That is not competitive with attention AGPT, but it shows attention is not structurally required. Phase-weighted W=16 did not beat it (6.40 at epoch 512), and stop-gradient singleton backoff was worse (7.83 vs 7.25 at epoch 100).

Plain tanh d8/d64 pd=1, epoch 500 6.3099 legacy held-out rolling PPL (agpt_recur_perplexity, seq 8, clean 95/5 split) pre-race-fixlegacy-eval
Phase-weighted W=16, epoch 512 6.4035 legacy held-out rolling PPL (agpt_recur_perplexity, seq 8, clean 95/5 split) pre-race-fixlegacy-eval
Singleton-backoff stopgrad, epoch 100 7.8265 legacy held-out rolling PPL (agpt_recur_perplexity, seq 8, clean 95/5 split) pre-race-fixlegacy-eval
TOPICS
RELATED
SUPERSEDED BY
CODE
THE FULL RECORD

Experiment notes

Open directory on GitHub ↗

Tanh Recurrent AGPT

Status: closed branch tanh-recurrence.

This line tests AGPT as a framework rather than as attention specifically:

h_child = tanh(W_h h_parent + W_x emb[token] + b)
logits = W_o h_child + c_o

The trainer is bin/agpt_train_recur, implemented in src/tools/agpt_train_recur.cr. Held-out evaluation uses bin/agpt_recur_perplexity.

Close-Out

This R&D line answered its main question: f_theta is free in the AGPT framework. Attention is not structurally required. Replacing attention with a simple recurrent tanh transition trains cleanly over the same prefix/mass framework:

h_child = tanh(W_h h_parent + W_x emb[token] + b)

The best clean tanh result in this branch is 6.3099 heldout PPL at depth 8, d_model=64, partition-depth=1, epoch 500. This is not competitive with the best attention AGPT runs, but it is strong enough to establish that AGPT is a framework over trainable prefix transitions, not an attention-only implementation.

The next research line is stride/dilated trees: use the same freedom in f_theta to test whether longer-range structure can be exposed through skip-step recurrence or auxiliary stride states.

Clean Methodology

All reportable recurrent results in this file use a trie and any position tables built from the train split only:

train:   /home/trans/Projects/agpt/data/.splits/4fa9aec1db6b3aea/train_corpus.txt
heldout: /home/trans/Projects/agpt/data/.splits/4fa9aec1db6b3aea/heldout_corpus.txt
vocab:   data/input.txt

Train-only artifacts:

prefix trie:   /tmp/agpt_tanh_clean_d8_radix
suffix trie:   /tmp/agpt_tanh_clean_d8_suffix_radix
W=16 position: /tmp/agpt_tanh_clean_d8_w16_position_data

The clean d8 prefix trie has 814,759 records and 7,038,366 loss events.

Deferred rerun queue:

  • Phase-embedded/phase-weighted W=16 pd=1 lr=0.001 to 512.
  • Phase-weighted W=32 pd=1 lr=0.001 to 512.
  • Plain tanh pd=0 full-batch lr=0.001 to 500.
  • RMSNorm clean rerun, if comparison against Claude's variants needs it.

Clean Results

All runs below use depth 8, d_model=64, Adam, constant lr=0.001, seed 1, partition-depth=1, and held-out rolling eval with seq-len=8, unless noted.

variant epoch train PPL clean heldout PPL bpc status
plain tanh 100 8.4876 7.2456 2.8571 complete
plain tanh 500 7.7584 6.3099 2.6576 complete
phase-weighted W=16 128 8.4604 7.2145 2.8509 complete
phase-weighted W=16 256 8.0396 6.7060 2.7454 complete
phase-weighted W=16 512 7.8321 6.4035 2.6789 complete
phase-embedded/weighted W=16 100 8.5876 7.3624 2.8802 pending longer rerun
singleton-backoff stopgrad 100 8.8663 7.8265 2.9684 complete negative probe

Current read:

  • Plain tanh pd=1 is the clean baseline to beat: 6.3099 heldout PPL at epoch 500.
  • Phase-weighted W=16 is viable and improves steadily, but does not beat clean plain tanh by epoch 512.
  • Phase-embedded/weighted W=16 is slightly ahead of plain early at epoch 20, but behind by epoch 100; it still needs a clean 512-epoch run before closing that variant.
  • Stop-gradient singleton backoff is worse than plain tanh at matched epoch 100, so that exact implementation is not promising.

Plain Tanh pd=1

Command:

OPENBLAS_NUM_THREADS=1 nice -n 10 bin/agpt_train_recur \
  --trie /tmp/agpt_tanh_clean_d8_radix \
  --d-model 64 \
  --epochs 480 \
  --lr 0.001 \
  --seed 1 \
  --load /tmp/agpt_tanh_clean_d8_d64_pd1_lr001_20.recur \
  --save /tmp/agpt_tanh_clean_d8_d64_pd1_lr001_500.recur \
  --partition-depth 1 \
  --checkpoint-every 50

Clean held-out sweep:

epoch train PPL clean heldout PPL bpc
10 12.6756 11.7094 3.5496
20 10.9332 10.0680 3.3317
50 9.3819 8.2995 3.0530
100 8.4876 7.2456 2.8571
150 8.1717 6.8447 2.7750
200 8.0084 6.6547 2.7344
250 7.9143 6.5466 2.7107
300 7.8553 6.4460 2.6884
350 7.8186 6.3972 2.6774
400 7.7851 6.3457 2.6658
450 7.7631 6.3385 2.6641
500 7.7584 6.3099 2.6576

partition-depth=1 splits by root-child subtree, giving 65 Adam updates per epoch on the Tiny Shakespeare character vocabulary. This is the main reason the pd=1 model improves much faster than full-batch pd=0.

Phase-Weighted W=16

Phase weighting is a training-measure change, not an attention/RoPE-specific mechanism. This variant keeps the global target distribution at each prefix and replaces the node's loss mass with its mass at the current corpus-position phase:

q_global(token | prefix) = global_count(prefix, token) / global_mass(prefix)
loss_weight(prefix, phase) = position_mass(prefix, phase)

No phase embedding or architecture change is used.

Command:

OPENBLAS_NUM_THREADS=1 nice -n 10 bin/agpt_train_recur \
  --trie /tmp/agpt_tanh_clean_d8_radix \
  --d-model 64 \
  --epochs 512 \
  --lr 0.001 \
  --seed 1 \
  --save /tmp/agpt_tanh_clean_phaseweighted_w16_d8_d64_pd1_lr001_512.recur \
  --partition-depth 1 \
  --phase-weighted-position-data /tmp/agpt_tanh_clean_d8_w16_position_data \
  --checkpoint-every 32

Clean held-out sweep:

epoch phase-weighted train PPL clean heldout PPL bpc
32 10.1772 9.1968 3.2011
64 9.1654 8.0369 3.0066
96 8.7110 7.5077 2.9084
128 8.4604 7.2145 2.8509
160 8.3018 7.0206 2.8116
192 8.1899 6.8789 2.7822
224 8.1029 6.7782 2.7609
256 8.0396 6.7060 2.7454
288 7.9292 6.6415 2.7315
320 7.9044 6.5901 2.7203
352 7.8807 6.5550 2.7126
384 7.8758 6.5112 2.7029
416 7.8766 6.4815 2.6963
448 7.8611 6.4515 2.6896
480 7.8440 6.4233 2.6833
512 7.8321 6.4035 2.6789

Conclusion: phase-weighted W=16 is healthy and close, but not a clean win over plain tanh at this setting.

Phase-Embedded / Weighted W=16

--phase-conditioned-position-data DIR is currently a phase-embedded, phase-weighted variant. The name is unfortunate: it does not train phase-conditioned target distributions. It keeps global targets and adds a learned phase vector to the recurrent transition:

h_child = tanh(W_h h_parent + W_x emb[token] + phase_emb[(start_phase + depth) mod W] + b)

For compressed radix edges, the phase advances token by token. For held-out rolling evaluation, bin/agpt_recur_perplexity reads the phase-embedded checkpoint and uses the actual held-out corpus position q mod W for each consumed context token.

Clean 100-epoch probe:

OPENBLAS_NUM_THREADS=1 nice -n 10 bin/agpt_train_recur \
  --trie /tmp/agpt_tanh_clean_d8_radix \
  --d-model 64 \
  --epochs 80 \
  --lr 0.001 \
  --seed 1 \
  --load /tmp/agpt_tanh_clean_phasecond_w16_d8_d64_pd1_lr001_20.recur \
  --save /tmp/agpt_tanh_clean_phaseembed_w16_d8_d64_pd1_lr001_100.recur \
  --partition-depth 1 \
  --phase-conditioned-position-data /tmp/agpt_tanh_clean_d8_w16_position_data \
  --checkpoint-every 20
epoch phase-embedded train PPL clean heldout PPL bpc
10 12.3170 11.5589 3.5309
20 10.8034 9.9197 3.3103
40 9.7798 8.7086 3.1224
60 9.1405 8.0517 3.0093
80 8.8219 7.6205 2.9299
100 8.5876 7.3624 2.8802

Clean read: explicit phase embeddings help slightly early, but are behind plain tanh by epoch 100. The matched 512-epoch rerun is still pending.

Singleton Cap Backoff Analysis

Question: if a radix end cap becomes singleton deterministic (mass=1, H=0), can we jump to a suffix/backoff position and continue from a reusable context instead of training through the unique cap?

Clean d8 trie singleton structure:

total records:              814,759
singleton deterministic:    448,685 records (55.07%)
singleton loss events:      448,685 / 7,038,366 (6.37%)
singleton edge chars:       981,177
mean singleton edge length: 2.187 chars

Singleton edge-length histogram:

edge len records
1 153,498
2 136,420
3 97,347
4 46,189
5 13,424
6 1,728
7 78
8 1

For each character position inside a singleton cap, we recursively dropped the oldest context token until the suffix context had mass greater than one. Example:

A B C D [E F G]  -> at E, try B C D E, then C D E, then D E...
                    stop at the first suffix context with mass > 1

Results over 981,176 singleton-cap positions:

resolved to mass > 1 suffix:       979,778 (99.86%)
unresolved before empty context:     1,398 (0.14%)
mean token drops needed:              2.427
mean target context length:           4.689
mean target mass:                   207.032
target entropy > 0:                979,778 (100.00%)

Drop-count histogram:

drops positions
1 301,028
2 267,698
3 204,049
4 126,731
5 60,998
6 17,194
7 2,080

Target context-length histogram:

target len positions
1 5,209
2 44,327
3 145,280
4 236,716
5 264,107
6 191,697
7 92,442

Target mass histogram:

mass bucket positions
2-4 247,465
5-16 294,574
17-64 222,361
>64 215,378

Interpretation: recursive suffix backoff almost always escapes singleton cap tunnels into a reusable, branching context. This is much stronger than simple one-token backoff. It makes suffix backoff a plausible structural variant, not just an optimization, but the first training probe below did not help.

Hard Backoff Prototype

bin/agpt_train_recur --singleton-backoff implements the first hard-routing prototype:

when a singleton cap token is consumed:
  replace the local state with the precomputed recursive suffix target state
  continue through the remaining edge from that reusable suffix state

Backward pass semantics:

gradient after the route is credited to the backoff target endpoint
gradient does not flow through the discarded singleton tunnel

The fully routed version requires --partition-depth 0. With partition-depth=1, a backoff target can live under a different root-child partition, so the target state and reverse pass would not necessarily be present in the same optimizer batch.

Dry-run on the clean d8 trie:

singleton_backoff: on (routes=979778, unresolved=1398)

Full-trie one-epoch smoke, d_model=16, pd=0:

epoch 1  nll 4.175799  ppl 65.091810  events 7038366  updates 1

--singleton-backoff-stopgrad adds the pd=1-compatible probe variant:

route hidden states through the recursive suffix target
stop gradient at the route boundary
preserve partition-depth=1 update cadence

A 100-epoch clean probe:

OPENBLAS_NUM_THREADS=1 nice -n 10 bin/agpt_train_recur \
  --trie /tmp/agpt_tanh_clean_d8_radix \
  --d-model 64 \
  --epochs 100 \
  --lr 0.001 \
  --seed 1 \
  --save /tmp/agpt_tanh_clean_backoff_stopgrad_d8_d64_pd1_lr001_100.recur \
  --partition-depth 1 \
  --singleton-backoff-stopgrad \
  --checkpoint-every 20
epoch stopgrad train PPL clean heldout PPL bpc
20 11.0025 10.2141 3.3525
40 9.8831 9.0039 3.1706
60 9.3691 8.3767 3.0664
80 9.0642 8.0535 3.0096
100 8.8663 7.8265 2.9684

This is a negative result versus clean plain tanh at matched epoch 100:

clean plain tanh epoch 100:        7.2456
backoff stopgrad epoch 100:        7.8265
phase-embedded W=16 epoch 100:     7.3624

Stop-gradient routing preserves the pd=1 update cadence, but the missing gradient through the reusable suffix targets appears to matter. The fully correct hard-backoff route still requires either pd=0 or a new partitioning scheme that carries cross-partition backoff dependencies.

Open Questions

  • Compare clean tanh recurrence against Claude's clean linear, linear+RMSNorm, and GRU variants with identical split, depth, d_model, optimizer, and held-out evaluator.
  • Complete the clean phase-embedded W=16 run to 512 before closing phase inputs.
  • Decide whether W=32 is still worth a clean rerun now that W=16 did not beat plain tanh.
  • Try depth 16 only after the depth-8 clean result table is stable.
  • Add K-FAC or a related curvature-aware optimizer. This model is small enough that a structured second-order method may be practical.
  • Explore stride/dilated trees as a grounded way to expose longer-range structure to recurrent f_theta.