{"slug": "i-made-an-llm-think-in-latent-space-the-model-never-read-a-single-thought", "title": "I made an LLM think in latent space. The model never read a single thought", "summary": "A developer tested an aggressive version of latent-space reasoning on a 1.5B-parameter language model, where the model pauses during generation to run parallel hidden-state rollouts without decoding tokens. Standard metrics like loss and benchmark scores showed improvement, but diagnostic tools revealed the latent states drifted to junk tokens and the gains came from generic fine-tuning, not from the latent reasoning mechanism.", "body_md": "# I tried to make an LLM think in latent space. The metrics said it worked. The model never read a single thought.\n\nStandard LLM reasoning pays a strange tax. Every step of a chain of thought gets squeezed through the vocabulary: hidden state to logits, sample a token, embed the token, continue. Two lossy projections per step, and the whole trajectory of \"thought\" is constrained to hop between ~150k discrete points in a continuous space. Papers like Coconut (Meta, 2024) asked the obvious question: what if the model just kept thinking in hidden-state space and only decoded at the end?\n\nI spent a couple of weeks and about 21 GPU-hours on a 12GB laptop GPU testing an aggressive version of this idea, and I want to write up what happened, because the interesting part isn't that it failed. It's *how confidently the standard metrics told me it was working while it failed*.\n\nThe setup, on Qwen2.5-1.5B-Instruct: at moments of uncertainty during generation (next-token entropy spike), pause. Take the current hidden state, clone it into K parallel streams, add a different noise vector to each. Let each stream \"think\" by feeding its final-layer hidden state back in as the next input embedding, N steps, no decoding. Then compress the streams through a bottleneck, inject the result into the context as extra positions, and resume generating.\n\nThere's a whole story in the design about parallel subconscious processing and global-workspace bottlenecks, but mechanically that's it: recurrent latent rollouts, noise for diversity, inject, decode.\n\nBefore any training, I built two diagnostics that turned out to be the most valuable code in the project.\n\nThe first was a **logit lens over latent states**: at every latent step, project each stream's hidden state through the output head and log the top tokens. Not to generate, just to see what region of token space the state occupies.\n\nThe result was immediate. At step 0 the states decode to task-flavoured tokens (\"Step\", \"answer\", digits). Within 2 to 4 latent steps they drift to junk: whitespace, `/router`\n\n, `Array`\n\n, random CJK fragments. Task-token fraction drops from about 8% to about 1%. The trajectories cover a spectacular amount of hidden-space volume (about 1000x the convex-hull volume of a token-level CoT trajectory in top-5 PCA dims) but the volume is empty. Reach is not meaning.\n\nWorse, the architecture had a built-in uncertainty story: streams disagreeing means the model is uncertain, streams converging means consensus, so use variance as a halt signal. The lens killed that too. Stream agreement does rise as variance falls, but they agree on junk tokens. What looks like consensus is all the streams falling into the same content-free attractor of the frozen recurrent map. If I had shipped variance-convergence as a feature without the lens, I'd have been measuring collapse and calling it agreement.\n\nFine, nobody expected the frozen model to do this. Coconut needed curriculum training. So: train it.\n\nThe second diagnostic mattered here. At every evaluation, I decoded each held-out problem three ways: (a) normally, (b) with the latent states **zeroed** before injection, (c) with latent states **shuffled** across problems, so each question gets another question's thoughts. If (a) isn't better than (b) and (c), the latent channel is decorative, whatever the other metrics say.\n\nThen I climbed the ladder. Each rung is a thing that plausibly fixes the previous rung's failure:\n\n**Boundary adapter**(4.7M params): a learned projection at the feedback and injection boundaries, because raw hidden states are out-of-distribution as embeddings. Loss fell 4.3 to 1.3. Benchmark: 18.4% on GSM8K vs 16.8% untrained. Ablations: zeros*beat*normal. The adapter had learned to emit a useful constant bias vector. Prompt tuning in a trench coat.**LoRA on top**(23M params), Coconut curriculum stage 1: loss 0.53, benchmark 26.0%. Progress! Ablations: no separation. The gain was generic fine-tuning on GSM8K text. The latent channel: still unread.**Deeper curriculum**(stage 2, more CoT hidden behind latent blocks): benchmark*dropped*to 17.6%. Forcing the model to rely on a channel that carries nothing just removes information.**Direct supervision + forcing**: CODI-style distillation (pull each latent step's state toward the teacher's hidden state at the corresponding written CoT step) plus context dropout (mask 30% of the visible CoT so the decoder has to look somewhere else). This was the make-or-break run.\n\nThe distillation *worked on its own terms*, and this is the part I find genuinely interesting. Distill loss fell 1.98 to 0.42. Lens entropy of the latent states dropped from 6.5 nats (diffuse noise) to 2.2 (sharp, structured). The latent states are absolutely shapeable. Content was going in.\n\nMid-run, the step-800 eval showed normal ahead of both ablations for the first time in the entire project. I'll be honest: I got excited.\n\nThe final eval: normal 20%, zeros 30%, shuffle 20%. The benchmark: 18.4%, a 0.8pp recovery of the 8.4pp the deeper curriculum had cost. Across 12 ablation evaluations spanning every training configuration, zeroing or shuffling the latent thoughts matched or beat using them in 11.\n\nWrite side: solved. Read side: the decoder never once conditioned on the channel. Cross-entropy always found a lower-loss path that ignores the injected positions, even with a third of the visible reasoning masked.\n\nFor completeness I also tested depth recurrence: loop a block of transformer layers R times per position during ordinary generation, no latent streams at all. Inference-only, static loop count. R=1 reproduces the stock model (30.8% vs 30.0% baseline, logit-level allclose). R=2: 28.8%, basically neutral. R=4: **6.0%**. There's a cliff between two and four iterations where re-applied layers stop refining and start destroying. Models like Huginn that make depth recurrence work had it present during pretraining. You don't get it free at inference.\n\nThe token bottleneck I set out to remove kept winning: token CoT hit 30% at one-sixth the FLOPs of any latent configuration. My current best guess is that the bottleneck is doing real work. Discretizing at every step forces commitment and re-grounds the trajectory in the distribution the model was trained on. The continuous loop has no such mechanism, and at LoRA scale, nothing I tried could train one in.\n\nBut the transferable lesson is about evaluation. Over this project, the following signals all looked like progress: falling training loss, falling distillation loss, a 3x sharpening of latent-state lens entropy, a 9pp benchmark improvement, and one tantalizing mid-run eval. Every one of them was real, measured, reproducible. And the two-line ablation (zero the states, shuffle the states) showed that none of them meant what they appeared to mean. I don't think this failure mode is unique to my kitchen-table setup. If you're evaluating any architecture that claims models \"reason in latent space\", ask whether anyone zeroed the latents. The papers I was replicating ideas from mostly don't report that control.\n\nCaveats, stated plainly: one model (1.5B), one task family (GSM8K), one seed per configuration, parameter-efficient training only. Full fine-tuning is exactly the regime where published parity claims live, and I couldn't afford it. The strongest untested lever in my own setup is concat aggregation (injecting all K streams as separate positions instead of mean+variance, which may destroy content before the decoder ever sees it). If someone runs either, I'd genuinely love to be wrong.\n\nCode, training logs, all run data, and the full technical report: [github.com/Oli-26/LatentReasoningNoDecode](https://github.com/Oli-26/LatentReasoningNoDecode)\n\n*Oliver Holder, July 2026*", "url": "https://wpnews.pro/news/i-made-an-llm-think-in-latent-space-the-model-never-read-a-single-thought", "canonical_source": "https://github.com/Oli-26/LatentReasoningNoDecode/blob/main/BLOG.md", "published_at": "2026-07-10 15:50:34+00:00", "updated_at": "2026-07-10 16:05:16.669896+00:00", "lang": "en", "topics": ["large-language-models", "ai-research"], "entities": ["Qwen2.5-1.5B-Instruct", "Meta", "Coconut"], "alternates": {"html": "https://wpnews.pro/news/i-made-an-llm-think-in-latent-space-the-model-never-read-a-single-thought", "markdown": "https://wpnews.pro/news/i-made-an-llm-think-in-latent-space-the-model-never-read-a-single-thought.md", "text": "https://wpnews.pro/news/i-made-an-llm-think-in-latent-space-the-model-never-read-a-single-thought.txt", "jsonld": "https://wpnews.pro/news/i-made-an-llm-think-in-latent-space-the-model-never-read-a-single-thought.jsonld"}}