Standard chain-of-thought (CoT) forces models to serialize their reasoning into discrete tokens — a fundamental bottleneck. Coconut replaces explicit reasoning steps with continuous hidden-state vectors that are fed directly back as input embeddings:
Stage 0: [Q] step₁ step₂ step₃ step₄ #### answer ← Full CoT (standard SFT)
Stage 1: [Q] <bot> h h <eot> step₂ step₃ step₄ #### answer ← 1st step → latent
Stage 2: [Q] <bot> h h h h <eot> step₃ step₄ #### answer ← 2 steps → latent
Stage 3: [Q] <bot> h h h h h h <eot> step₄ #### answer ← 3 steps → latent
Stage 4: [Q] <bot> h h h h h h h h <eot> #### answer ← ALL steps → latent
Each h is a continuous thought — the model's last-layer hidden state fed directly as the next input embedding. The optimizer is completely reset at each stage transition.
For n latent thoughts, there are n+1 sequential forward passes. Each pass generates the hidden state that feeds into the next latent position.
Training Recipe (from paper)
Parameter
Value
Base model
GPT-2 (124M)
Dataset
GSM8k (7.5K examples)
c (thoughts per step)
2
Curriculum stages
4 + stage 0
Stage 0 epochs
6
Per-stage epochs
3
Total epochs
50
Learning rate
1e-4
Effective batch size
128
Optimizer
AdamW (reset each stage!)
Paper Results
Method
GSM8k
ProntoQA
ProsQA
No CoT
16.5%
93.8%
76.7%
Standard CoT
42.9%
98.8%
77.5%
Coconut
34.1%
99.8%
97.0%
Key insight: Coconut excels on planning/search tasks (ProsQA: +19.5% over CoT) where BFS-like breadth in latent space is advantageous.
Usage
Inference with Latent Thoughts
python
1import torch
2from transformers import GPT2LMHeadModel, GPT2Tokenizer
34model = GPT2LMHeadModel.from_pretrained("blanar/coconut-gsm8k-gpt2")5tokenizer = GPT2Tokenizer.from_pretrained("blanar/coconut-gsm8k-gpt2")67bot_id = tokenizer.convert_tokens_to_ids("<|start-latent|>")8eot_id = tokenizer.convert_tokens_to_ids("<|end-latent|>")910question ="If a store sells 3 apples for $2, how much do 12 apples cost?"11q_tokens = tokenizer.encode(f"Question: {question}\nAnswer: ")1213# Build input with <bot> token14input_embeds = model.transformer.wte(torch.tensor([q_tokens]))15bot_embed = model.transformer.wte(torch.tensor([[bot_id]]))16current = torch.cat([input_embeds, bot_embed], dim=1)1718# Generate 8 latent thoughts (continuous hidden states — no text!)19n_latent =820for _ inrange(n_latent):21 out = model(inputs_embeds=current, output_hidden_states=True)22 h = out.hidden_states[-1][:,-1:,:]# last hidden state23 current = torch.cat([current, h], dim=1)# feed back as input2425# Switch back to text mode with <eot>26eot_embed = model.transformer.wte(torch.tensor([[eot_id]]))27current = torch.cat([current, eot_embed], dim=1)2829# Greedy decode the answer (now in normal text mode)30for _ inrange(100):31 out = model(inputs_embeds=current)32 next_token = out.logits[:,-1,:].argmax(-1)33if next_token.item()== tokenizer.eos_token_id:34break35 next_embed = model.transformer.wte(next_token.unsqueeze(0))36 current = torch.cat([current, next_embed], dim=1)