Skip to content

Commit 37a6869

Browse files
committed
olmo2: stop generation at the end-of-sequence token
A 200-token run walked straight through <|endoftext|> and started a fresh document mid-response, which is what a base model does when nothing stops it. The runner now reads eos_token_id from config.json and stops before emitting the sentinel rather than after, because a caller wants the text and not the marker. The serving lane does the same. Also verified while looking: 200 tokens stay finite with no NaN and no collapse, so the f16 residual holds across sixteen post-norm layers, and the same prompt produces bit-identical output across runs. Error paths all explain themselves rather than failing obscurely. A Qwen config is refused by name, a missing file by path, and a SmolLM-shaped head_dim 64 with the reason it cannot work here.
1 parent 4e13954 commit 37a6869

2 files changed

Lines changed: 24 additions & 0 deletions

File tree

crates/mainarch-cli/src/olmo_lane.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,17 @@ impl OlmoLane {
157157
continue;
158158
}
159159

160+
let eos = runner.eos_token();
160161
let mut ttft_ms = 0.0f64;
161162
let mut generated = 0usize;
162163
for i in 0..req.max_new {
163164
if i == 0 {
164165
ttft_ms = started.elapsed().as_secs_f64() * 1e3;
165166
}
167+
// Stop before emitting the sentinel, not after.
168+
if Some(next) == eos {
169+
break;
170+
}
166171
let piece = tokenizer
167172
.decode(&[next], false)
168173
.unwrap_or_else(|_| String::new());

crates/mainarch-core/src/olmo2.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ pub struct Olmo2Config {
9393
pub vocab_size: u64,
9494
pub rope_theta: u64,
9595
pub tie_word_embeddings: bool,
96+
/// End-of-sequence token, when config.json declares one.
97+
pub eos_token_id: Option<u64>,
9698
}
9799

98100
impl Olmo2Config {
@@ -132,6 +134,10 @@ impl Olmo2Config {
132134
.map(|v| v.as_bool("tie_word_embeddings"))
133135
.transpose()?
134136
.unwrap_or(false),
137+
eos_token_id: obj
138+
.get("eos_token_id")
139+
.map(|v| v.as_u64("eos_token_id"))
140+
.transpose()?,
135141
})
136142
}
137143

@@ -378,6 +384,7 @@ pub fn synthetic_olmo2_config(num_hidden_layers: u64) -> Olmo2Config {
378384
vocab_size: 100_352,
379385
rope_theta: 500_000,
380386
tie_word_embeddings: false,
387+
eos_token_id: Some(100_257),
381388
}
382389
}
383390

@@ -1552,6 +1559,11 @@ impl Olmo2Runner {
15521559
Ok(best)
15531560
}
15541561

1562+
/// The configured end-of-sequence token, if the checkpoint declares one.
1563+
pub fn eos_token(&self) -> Option<u32> {
1564+
self.weights.config.eos_token_id.map(|e| e as u32)
1565+
}
1566+
15551567
/// Greedy generation. The prompt is consumed one token at a time through the
15561568
/// decode path, which is prefill done the slow honest way: no prefill GEMM
15571569
/// kernel is needed because the decode loop already grows the KV cache.
@@ -1570,8 +1582,15 @@ impl Olmo2Runner {
15701582
next = self.step(dev, t, pos)?;
15711583
pos += 1;
15721584
}
1585+
let eos = self.weights.config.eos_token_id.map(|e| e as u32);
15731586
let mut out = Vec::with_capacity(max_new);
15741587
for _ in 0..max_new {
1588+
// Stop *before* emitting EOS. A caller wants the text, not the
1589+
// sentinel, and a base model that runs past it starts a new
1590+
// document mid-response.
1591+
if Some(next) == eos {
1592+
break;
1593+
}
15751594
out.push(next);
15761595
if pos as usize >= self.max_seq as usize {
15771596
break;

0 commit comments

Comments
 (0)