looped-qwen3-9.4M Β· 32 useful loops

Depth scaling: the saturation wall and its removal

A 9.44M-parameter looped transformer in which all 32 loops earn their keep. A 2-layer Qwen3-style core is applied 32 times with shared weights β€” 64 effective layers from 2 layers of parameters β€” trained from scratch on 100M FineWeb tokens.

Naive weight-tied looping does not merely saturate: past T=8 extra depth actively hurts, and T=64 is worse than T=2 at 24Γ— the compute. Two parameter-free changes remove that wall:

  1. depth-RoPE β€” the state is conjugated by a step-dependent channel rotation, so the block sees a different coordinate frame at every loop and the effective weight matrix differs per step. Zero learned parameters.
  2. Constant-share input injection β€” the token embedding keeps a fixed share of the block input at every loop, instead of decaying like 1/t as the state norm grows. One learned scalar.

With the wall removed, trained-and-evaluated-at-T quality is monotone through T=64 (4.118 β†’ 4.081 β†’ 4.059 nats at T = 16/32/64 on 25M tokens), where at 25M tokens it beats an unshared 16-layer model with 4.9Γ— the parameters (4.078). The learned versions of the same ideas β€” per-step embeddings, per-step adaLN β€” consistently hurt: step symmetry has to be broken structurally, not bought with capacity.

Results

model params tokens val loss (nats) ppl bits/byte
this checkpoint (T=32, lr 2e-3) 9.44M 100M 3.7219 41.34 1.4275
same recipe, lr 1.5e-3 9.44M 100M 3.7329 41.80 1.4317
unshared 16-layer reference (out of budget) 46.2M 100M 3.5938 36.37 1.3784
best naive loop, any depth (T=8) 9.44M 25M 4.2028 66.89 1.6119
naive loop at T=32 9.44M 25M 4.2880 72.82 1.6446

Robustness: 3 seeds per side at 25M tokens give 4.093 Β± 0.011 (recipe, T=32) against 4.220 Β± 0.016 (naive optimum, T=8) β€” an 11Οƒ gap.

Perplexity here is not comparable to other models. The tokenizer is a byte-level BPE with an 8,192-entry vocabulary trained on this dataset, because the 10M-parameter budget counts embeddings β€” Qwen3's 151,936-entry vocabulary would consume the entire budget before the transformer got a single layer. Compare bits per byte instead, which is tokenizer-independent. On this validation text the tokenizer yields 3.78 bytes/token (GPT-2: 4.48).

Why it works

Mechanism: collinear updates

Trajectory diagnostics of the naive loop show the failure is not DEQ-style convergence to a fixed point β€” update magnitude never decays. Instead successive updates become collinear (cosine β†’ 0.999 by loop 16) while the state norm grows exactly linearly: the loop spends its compute making one vector longer. In the coordinates every consumer actually sees (pre-norm), that is a fixed point β€” a ray in projective space. Factorial-shuffle probes show the block reads the state increasingly (state-driven share of its output: 0.25 β†’ 0.94 across loops) yet maps it to a nearly constant direction.

A step-dependent rotation makes repeating the same update geometrically unproductive. In the trained champion the successive-update cosine stays at 0.93 at loop 32 (naive: 0.999), the read-out improves at every loop (3.780 β†’ 3.722 over the last six), and inference-time depth transfers: the same weights run at any T, degrading gently beyond the trained depth.

Quick start

The architecture is custom, so load it with the code from the GitHub repository rather than AutoModel:

import json, torch
from safetensors.torch import load_file
from loopedlm.config import ModelConfig
from loopedlm.model import LoopedQwen3
from tokenizers import Tokenizer

cfg = ModelConfig.from_dict(json.load(open("model_config.json")))
model = LoopedQwen3(cfg).eval()
sd = load_file("model.safetensors")
sd["lm_head.weight"] = sd["embed_tokens.weight"]        # tied embeddings
model.load_state_dict(sd)
tok = Tokenizer.from_file("tokenizer.json")

ids = torch.tensor([tok.encode("The looped transformer").ids])
out = model(ids, n_loops=32)          # depth is a runtime choice
logits = model.head(out["hidden"])

n_loops is an inference-time argument β€” that is the point of the architecture. Run the same checkpoint shallower for cheap drafts or deeper for quality.

Architecture

Qwen3 recipe β€” pre-RMSNorm, per-head QK-norm, GQA, SwiGLU, RoPE, no biases, tied embeddings β€” with the middle of the network looped:

e   = EmbedNorm(Embed(idx))
h_0 = e
h_t = h_{t-1} + R_t^{-1} Β· Core( R_t Β· (h_{t-1}/rms + Ξ±Β·e) )     t = 1..32
logits = Head(RMSNorm(h_32))
parameters (total / non-embedding) 9,440,513 / 5,245,953
d_model / heads / KV heads / head dim 512 / 8 / 2 / 64
looped core / loops (train, max) 2 layers / 32, 64
MLP intermediate 1280 (SwiGLU)
context / vocab 512 / 8,192 (byte-level BPE, lossless)
depth conditioning depth-RoPE, half the channels, ΞΈ=1000
input injection constant-share (add_relative)

Training

  • Data: FineWeb sample/10BT, shard 000 for training, shard 014 for validation β€” disjoint documents; the tokenizer never saw the validation shard.
  • Budget enforcement: non-overlapping windows in a seeded permutation β€” every training token is seen exactly once, asserted against the measured count; the trainer refuses to start if the model exceeds 10M parameters.
  • Optimisation: AdamW, lr 2e-3 (the symmetry-broken loop prefers a hotter LR than the naive one β€” the optimum moves up, not down), cosine schedule, wd 0.1, clip 1.0, bf16 autocast, batch 32Γ—512, gradient checkpointing per loop.
  • Hardware: one A100 80GB, ~2.5 h wall-clock, torch.compile on the shared block (2.5Γ— measured).

Files

model.safetensors (weights, fp32), model_config.json / train_config.json (exact configs), tokenizer.json (HF tokenizers file), summary.json (final metrics + full test-time depth sweep), log.jsonl (complete training log).

Limitations

At 9.44M parameters and 100M tokens this is a research artefact for studying depth recurrence, not an assistant: it produces locally plausible English and nothing more. No instruction tuning, no safety alignment, English only, 512-token context.

Code, 94-run experiment log and the full report: github.com/Embim/Looped-lm

Downloads last month

-

Downloads are not tracked for this model. How to track
Safetensors
Model size
9.44M params
Tensor type
F32
Β·
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support

Dataset used to train Embim/looped-qwen3-9.4M-T32-fineweb

Evaluation results

  • Validation perplexity (own 8k byte-level BPE) on FineWeb (held-out shard, sample/10BT/014)
    self-reported
    41.340
  • Bits per byte (tokenizer-independent) on FineWeb (held-out shard, sample/10BT/014)
    self-reported
    1.427