1use std::ops::ControlFlow;
4use std::path::Path;
5
6use rand::rngs::StdRng;
7use rand::{Rng, SeedableRng};
8use serde::Deserialize;
9
10use crate::autograd::{TVar, Tape};
11use crate::device::Device;
12use crate::error::{ForgeError, Result};
13use crate::nn::{Embedding, LayerNorm, Linear};
14use crate::ops::{self, MatmulSpec};
15use crate::tensor::Tensor;
16use crate::tokenizer::Tokenizer;
17
18fn default_eps() -> f32 {
19 1e-5
20}
21
22#[derive(Debug, Clone)]
23pub struct Gpt2Config {
24 pub n_layer: usize,
25 pub n_head: usize,
26 pub n_embd: usize,
27 pub n_ctx: usize,
28 pub vocab_size: usize,
29 pub layer_norm_epsilon: f32,
30 pub eos_token_id: Option<u32>,
31}
32
33#[derive(Deserialize)]
35struct RawConfig {
36 n_layer: usize,
37 n_head: usize,
38 n_embd: usize,
39 #[serde(default)]
40 n_ctx: Option<usize>,
41 #[serde(default)]
42 n_positions: Option<usize>,
43 vocab_size: usize,
44 #[serde(default = "default_eps")]
45 layer_norm_epsilon: f32,
46 #[serde(default)]
47 eos_token_id: Option<u32>,
48}
49
50impl Gpt2Config {
51 pub fn gpt2() -> Self {
53 Gpt2Config {
54 n_layer: 12,
55 n_head: 12,
56 n_embd: 768,
57 n_ctx: 1024,
58 vocab_size: 50257,
59 layer_norm_epsilon: 1e-5,
60 eos_token_id: Some(50256),
61 }
62 }
63
64 pub fn from_json(path: impl AsRef<Path>) -> Result<Self> {
65 let text = std::fs::read_to_string(path)?;
66 Self::from_json_str(&text)
67 }
68
69 pub fn from_json_str(text: &str) -> Result<Self> {
72 let raw: RawConfig = serde_json::from_str(text)?;
73 let n_ctx = raw.n_ctx.or(raw.n_positions).ok_or_else(|| {
74 ForgeError::Json(serde::de::Error::custom("missing n_ctx/n_positions"))
75 })?;
76 Ok(Gpt2Config {
77 n_layer: raw.n_layer,
78 n_head: raw.n_head,
79 n_embd: raw.n_embd,
80 n_ctx,
81 vocab_size: raw.vocab_size,
82 layer_norm_epsilon: raw.layer_norm_epsilon,
83 eos_token_id: raw.eos_token_id,
84 })
85 }
86}
87
88struct Block {
89 ln_1: LayerNorm,
90 attn_qkv: Linear,
91 attn_proj: Linear,
92 ln_2: LayerNorm,
93 mlp_fc: Linear,
94 mlp_proj: Linear,
95}
96
97pub struct Gpt2 {
98 emb: Embedding,
99 blocks: Vec<Block>,
100 ln_f: LayerNorm,
101 pub config: Gpt2Config,
102}
103
104#[derive(Debug, Clone, Copy)]
105pub enum Sampling {
106 Greedy,
107 TopK {
108 k: usize,
109 temperature: f32,
110 seed: u64,
111 },
112}
113
114#[derive(Debug, Clone)]
122pub struct AttnStep {
123 pub layer: usize,
124 pub n_head: usize,
125 pub q_len: usize,
127 pub kv_len: usize,
129 pub probs: Vec<f32>,
130}
131
132#[derive(Debug, Clone)]
140pub struct LayerDetail {
141 pub layer: usize,
142 pub ln1_out: Vec<f32>,
144 pub q: Vec<f32>,
146 pub k: Vec<f32>,
147 pub v: Vec<f32>,
148 pub scores: Vec<f32>,
152 pub attn_head_out: Vec<f32>,
156 pub attn_proj_out: Vec<f32>,
158 pub resid_attn: Vec<f32>,
160 pub ln2_out: Vec<f32>,
162 pub mlp_hidden: Vec<f32>,
164 pub block_out: Vec<f32>,
166}
167
168#[derive(Debug, Clone)]
172pub struct StepTrace {
173 pub q_len: usize,
175 pub kv_len: usize,
177 pub n_head: usize,
178 pub n_embd: usize,
179 pub embedding: Vec<f32>,
182 pub attn: Vec<AttnStep>,
184 pub detail: Vec<LayerDetail>,
186 pub ln_f_out: Vec<f32>,
189 pub top: Vec<(u32, f32)>,
192}
193
194#[derive(Debug, Clone, Copy)]
197enum ProbeKind {
198 Embedding,
199 Ln1Out { layer: usize },
200 Query { layer: usize },
201 Key { layer: usize },
202 Value { layer: usize },
203 Scores { layer: usize },
204 Attention { layer: usize },
205 AttnHeadOut { layer: usize },
206 AttnProjOut { layer: usize },
207 ResidAttn { layer: usize },
208 Ln2Out { layer: usize },
209 MlpHidden { layer: usize },
210 BlockOut { layer: usize },
211 LnFOut,
212}
213
214struct Probe {
221 detail_layers: usize,
222 kinds: Vec<ProbeKind>,
223 tensors: Vec<Tensor>,
224}
225
226impl Probe {
227 fn new(detail_layers: usize) -> Self {
228 Probe {
229 detail_layers,
230 kinds: Vec::new(),
231 tensors: Vec::new(),
232 }
233 }
234
235 fn detail(&self, layer: usize) -> bool {
236 layer < self.detail_layers
237 }
238
239 fn push(&mut self, kind: ProbeKind, t: &Tensor) {
240 self.kinds.push(kind);
241 self.tensors.push(t.clone());
242 }
243}
244
245pub struct KvCache {
248 k: Vec<Tensor>,
249 v: Vec<Tensor>,
250 len: usize,
251}
252
253impl KvCache {
254 pub fn len(&self) -> usize {
255 self.len
256 }
257
258 pub fn is_empty(&self) -> bool {
259 self.len == 0
260 }
261}
262
263impl Gpt2 {
264 pub fn from_safetensors(
268 path: impl AsRef<Path>,
269 config: Gpt2Config,
270 device: &Device,
271 ) -> Result<Self> {
272 let bytes = std::fs::read(path.as_ref())?;
273 Self::from_safetensors_bytes(&bytes, config, device)
274 }
275
276 pub fn from_safetensors_bytes(
281 bytes: &[u8],
282 config: Gpt2Config,
283 device: &Device,
284 ) -> Result<Self> {
285 let st = safetensors::SafeTensors::deserialize(bytes)
286 .map_err(|e| ForgeError::SafeTensors(format!("deserialize: {e}")))?;
287 let view = |name: &str| -> Result<safetensors::tensor::TensorView<'_>> {
288 st.tensor(name)
289 .or_else(|_| st.tensor(&format!("transformer.{name}")))
290 .map_err(|_| ForgeError::SafeTensors(format!("missing tensor {name}")))
291 };
292 let host = |name: &str| -> Result<(Vec<usize>, Vec<f32>)> {
293 let v = view(name)?;
294 if v.dtype() != safetensors::tensor::Dtype::F32 {
295 return Err(ForgeError::SafeTensors(format!(
296 "{name}: expected f32, got {:?}",
297 v.dtype()
298 )));
299 }
300 Ok((v.shape().to_vec(), bytemuck::pod_collect_to_vec(v.data())))
301 };
302 let take = |name: &str| -> Result<Tensor> {
303 let (shape, data) = host(name)?;
304 Tensor::from_f32(&data, shape, device)
305 };
306 let eps = config.layer_norm_epsilon;
307 let (_, wte_host) = host("wte.weight")?;
310 let emb = Embedding::from_host_wte(
311 &wte_host,
312 config.vocab_size,
313 config.n_embd,
314 take("wpe.weight")?,
315 device,
316 )?;
317 drop(wte_host);
318 let mut blocks = Vec::with_capacity(config.n_layer);
319 for i in 0..config.n_layer {
320 blocks.push(Block {
321 ln_1: LayerNorm {
322 gamma: take(&format!("h.{i}.ln_1.weight"))?,
323 beta: take(&format!("h.{i}.ln_1.bias"))?,
324 eps,
325 },
326 attn_qkv: Linear {
327 w: take(&format!("h.{i}.attn.c_attn.weight"))?,
328 b: Some(take(&format!("h.{i}.attn.c_attn.bias"))?),
329 },
330 attn_proj: Linear {
331 w: take(&format!("h.{i}.attn.c_proj.weight"))?,
332 b: Some(take(&format!("h.{i}.attn.c_proj.bias"))?),
333 },
334 ln_2: LayerNorm {
335 gamma: take(&format!("h.{i}.ln_2.weight"))?,
336 beta: take(&format!("h.{i}.ln_2.bias"))?,
337 eps,
338 },
339 mlp_fc: Linear {
340 w: take(&format!("h.{i}.mlp.c_fc.weight"))?,
341 b: Some(take(&format!("h.{i}.mlp.c_fc.bias"))?),
342 },
343 mlp_proj: Linear {
344 w: take(&format!("h.{i}.mlp.c_proj.weight"))?,
345 b: Some(take(&format!("h.{i}.mlp.c_proj.bias"))?),
346 },
347 });
348 }
349 let ln_f = LayerNorm {
350 gamma: take("ln_f.weight")?,
351 beta: take("ln_f.bias")?,
352 eps,
353 };
354 Ok(Gpt2 {
355 emb,
356 blocks,
357 ln_f,
358 config,
359 })
360 }
361
362 fn attention(&self, block: &Block, h: &Tensor) -> Result<Tensor> {
363 let n_head = self.config.n_head;
364 let hd = self.config.n_embd / n_head;
365 let qkv = block.attn_qkv.forward(h)?; let (q, k, v) = ops::split_heads(&qkv, n_head)?; let att = ops::matmul(
368 &q,
369 &k,
370 None,
371 MatmulSpec {
372 trans_b: true,
373 alpha: 1.0 / (hd as f32).sqrt(),
374 ..Default::default()
375 },
376 )?; let att = ops::softmax(&att, true, 0)?;
378 let y = ops::matmul(&att, &v, None, MatmulSpec::default())?; let y = ops::merge_heads(&y)?; block.attn_proj.forward(&y)
381 }
382
383 #[allow(clippy::too_many_arguments)] fn attention_cached(
393 &self,
394 layer: usize,
395 block: &Block,
396 h: &Tensor,
397 k_cache: &mut Tensor,
398 v_cache: &mut Tensor,
399 len: usize,
400 mut probe: Option<&mut Probe>,
401 ) -> Result<Tensor> {
402 let n_head = self.config.n_head;
403 let hd = self.config.n_embd / n_head;
404 let qkv = block.attn_qkv.forward(h)?; let (q, k, v) = ops::split_heads(&qkv, n_head)?; if let Some(p) = probe.as_deref_mut().filter(|p| p.detail(layer)) {
407 p.push(ProbeKind::Query { layer }, &q);
408 p.push(ProbeKind::Key { layer }, &k);
409 p.push(ProbeKind::Value { layer }, &v);
410 }
411 let t = q.shape().dim(1);
412 ops::kv_append(k_cache, &k, len)?;
413 ops::kv_append(v_cache, &v, len)?;
414 let kv_len = len + t;
415 let scores = ops::matmul(
416 &q,
417 k_cache,
418 None,
419 MatmulSpec {
420 trans_b: true,
421 alpha: 1.0 / (hd as f32).sqrt(),
422 b_rows: Some(kv_len),
423 ..Default::default()
424 },
425 )?; if let Some(p) = probe.as_deref_mut().filter(|p| p.detail(layer)) {
430 p.push(ProbeKind::Scores { layer }, &scores);
431 }
432 let att = ops::softmax(&scores, true, len)?; if let Some(p) = probe.as_deref_mut() {
434 p.push(ProbeKind::Attention { layer }, &att);
435 }
436 let y = ops::matmul(
437 &att,
438 v_cache,
439 None,
440 MatmulSpec {
441 b_rows: Some(kv_len),
442 ..Default::default()
443 },
444 )?; let y = ops::merge_heads(&y)?;
446 if let Some(p) = probe.as_deref_mut().filter(|p| p.detail(layer)) {
450 p.push(ProbeKind::AttnHeadOut { layer }, &y);
451 }
452 let proj = block.attn_proj.forward(&y)?;
453 if let Some(p) = probe.filter(|p| p.detail(layer)) {
454 p.push(ProbeKind::AttnProjOut { layer }, &proj);
455 }
456 Ok(proj)
457 }
458
459 fn hidden(&self, ids: &[u32]) -> Result<Tensor> {
461 if ids.is_empty() {
462 return Err(ForgeError::Shape("empty token sequence".into()));
463 }
464 if ids.len() > self.config.n_ctx {
465 return Err(ForgeError::Shape(format!(
466 "sequence length {} exceeds n_ctx {}",
467 ids.len(),
468 self.config.n_ctx
469 )));
470 }
471 let device = self.emb.wpe.device();
472 let ids_t = Tensor::from_u32(ids, [ids.len()], &device)?;
473 let mut x = self.emb.forward(&ids_t, 0)?;
474 for block in &self.blocks {
475 let attn_out = self.attention(block, &block.ln_1.forward(&x)?)?;
476 x = ops::add(&x, &attn_out)?;
477 let mlp_in = block.ln_2.forward(&x)?;
478 let mlp_out = block
479 .mlp_proj
480 .forward(&ops::gelu(&block.mlp_fc.forward(&mlp_in)?)?)?;
481 x = ops::add(&x, &mlp_out)?;
482 }
483 self.ln_f.forward(&x)
484 }
485
486 fn hidden_cached(
491 &self,
492 ids: &[u32],
493 cache: &mut KvCache,
494 mut probe: Option<&mut Probe>,
495 ) -> Result<Tensor> {
496 if ids.is_empty() {
497 return Err(ForgeError::Shape("empty token sequence".into()));
498 }
499 let pos = cache.len;
500 if pos + ids.len() > self.config.n_ctx {
501 return Err(ForgeError::Shape(format!(
502 "sequence length {}+{} exceeds n_ctx {}",
503 pos,
504 ids.len(),
505 self.config.n_ctx
506 )));
507 }
508 let device = self.emb.wpe.device();
509 let ids_t = Tensor::from_u32(ids, [ids.len()], &device)?;
510 let mut x = self.emb.forward(&ids_t, pos)?;
511 if let Some(p) = probe.as_deref_mut().filter(|p| p.detail_layers > 0) {
514 p.push(ProbeKind::Embedding, &x);
515 }
516 for (layer, (block, (kc, vc))) in self
517 .blocks
518 .iter()
519 .zip(cache.k.iter_mut().zip(cache.v.iter_mut()))
520 .enumerate()
521 {
522 let ln1 = block.ln_1.forward(&x)?;
525 if let Some(p) = probe.as_deref_mut().filter(|p| p.detail(layer)) {
526 p.push(ProbeKind::Ln1Out { layer }, &ln1);
527 }
528 let attn_out =
529 self.attention_cached(layer, block, &ln1, kc, vc, pos, probe.as_deref_mut())?;
530 x = ops::add(&x, &attn_out)?;
531 if let Some(p) = probe.as_deref_mut().filter(|p| p.detail(layer)) {
532 p.push(ProbeKind::ResidAttn { layer }, &x);
533 }
534 let mlp_in = block.ln_2.forward(&x)?;
535 if let Some(p) = probe.as_deref_mut().filter(|p| p.detail(layer)) {
536 p.push(ProbeKind::Ln2Out { layer }, &mlp_in);
537 }
538 let mlp_hidden = ops::gelu(&block.mlp_fc.forward(&mlp_in)?)?;
539 if let Some(p) = probe.as_deref_mut().filter(|p| p.detail(layer)) {
540 p.push(ProbeKind::MlpHidden { layer }, &mlp_hidden);
541 }
542 let mlp_out = block.mlp_proj.forward(&mlp_hidden)?;
543 x = ops::add(&x, &mlp_out)?;
544 if let Some(p) = probe.as_deref_mut().filter(|p| p.detail(layer)) {
545 p.push(ProbeKind::BlockOut { layer }, &x);
546 }
547 }
548 cache.len += ids.len();
549 let out = self.ln_f.forward(&x)?;
550 if let Some(p) = probe.filter(|p| p.detail_layers > 0) {
551 p.push(ProbeKind::LnFOut, &out);
552 }
553 Ok(out)
554 }
555
556 pub fn new_cache(&self) -> Result<KvCache> {
558 let device = self.emb.wpe.device();
559 let hd = self.config.n_embd / self.config.n_head;
560 let shape = [self.config.n_head, self.config.n_ctx, hd];
561 let mut k = Vec::with_capacity(self.config.n_layer);
562 let mut v = Vec::with_capacity(self.config.n_layer);
563 for _ in 0..self.config.n_layer {
564 k.push(Tensor::zeros(shape, &device)?);
565 v.push(Tensor::zeros(shape, &device)?);
566 }
567 Ok(KvCache { k, v, len: 0 })
568 }
569
570 pub fn forward(&self, ids: &[u32]) -> Result<Tensor> {
573 let h = self.hidden(ids)?;
574 ops::matmul_chunked_transb(&h, &self.emb.wte_chunks, 1.0)
575 }
576
577 pub fn logits_last(&self, ids: &[u32]) -> Result<Vec<f32>> {
580 let h = self.hidden(ids)?;
581 let last = h.narrow_rows(ids.len() - 1, 1)?;
582 ops::matmul_chunked_transb(&last, &self.emb.wte_chunks, 1.0)?.to_vec_f32()
583 }
584
585 pub fn logits_step(&self, ids: &[u32], cache: &mut KvCache) -> Result<Vec<f32>> {
588 let h = self.hidden_cached(ids, cache, None)?;
589 let last = h.narrow_rows(ids.len() - 1, 1)?;
590 ops::matmul_chunked_transb(&last, &self.emb.wte_chunks, 1.0)?.to_vec_f32()
591 }
592
593 pub async fn logits_step_async(&self, ids: &[u32], cache: &mut KvCache) -> Result<Vec<f32>> {
596 let h = self.hidden_cached(ids, cache, None)?;
597 let last = h.narrow_rows(ids.len() - 1, 1)?;
598 ops::matmul_chunked_transb(&last, &self.emb.wte_chunks, 1.0)?
599 .to_vec_f32_async()
600 .await
601 }
602
603 pub async fn logits_step_attn_async(
610 &self,
611 ids: &[u32],
612 cache: &mut KvCache,
613 ) -> Result<(Vec<f32>, Vec<AttnStep>)> {
614 let (logits, trace) = self.logits_step_trace_async(ids, cache, 0, 0).await?;
615 Ok((logits, trace.attn))
616 }
617
618 pub async fn logits_step_trace_async(
636 &self,
637 ids: &[u32],
638 cache: &mut KvCache,
639 detail_layers: usize,
640 top_n: usize,
641 ) -> Result<(Vec<f32>, StepTrace)> {
642 let q_len = ids.len();
643 let detail_layers = detail_layers.min(self.config.n_layer);
644 let mut probe = Probe::new(detail_layers);
645 let h = self.hidden_cached(ids, cache, Some(&mut probe))?;
646 let last = h.narrow_rows(q_len - 1, 1)?;
647 let Probe {
648 kinds,
649 mut tensors,
650 detail_layers,
651 } = probe;
652 let shapes: Vec<Vec<usize>> = tensors.iter().map(|t| t.shape().dims().to_vec()).collect();
655 tensors.push(ops::matmul_chunked_transb(
656 &last,
657 &self.emb.wte_chunks,
658 1.0,
659 )?);
660
661 let mut read = Tensor::to_vec_f32_batch(&tensors).await?;
662 let logits = read.pop().expect("logits were pushed last");
663
664 let mut trace = StepTrace {
665 q_len,
666 kv_len: cache.len,
667 n_head: self.config.n_head,
668 n_embd: self.config.n_embd,
669 embedding: Vec::new(),
670 attn: Vec::with_capacity(self.config.n_layer),
671 detail: (0..detail_layers)
672 .map(|layer| LayerDetail {
673 layer,
674 ln1_out: Vec::new(),
675 q: Vec::new(),
676 k: Vec::new(),
677 v: Vec::new(),
678 scores: Vec::new(),
679 attn_head_out: Vec::new(),
680 attn_proj_out: Vec::new(),
681 resid_attn: Vec::new(),
682 ln2_out: Vec::new(),
683 mlp_hidden: Vec::new(),
684 block_out: Vec::new(),
685 })
686 .collect(),
687 ln_f_out: Vec::new(),
688 top: top_probs(&logits, top_n),
689 };
690 for ((kind, dims), data) in kinds.iter().zip(&shapes).zip(read) {
691 match *kind {
692 ProbeKind::Embedding => trace.embedding = data,
693 ProbeKind::Ln1Out { layer } => trace.detail[layer].ln1_out = data,
694 ProbeKind::Query { layer } => trace.detail[layer].q = data,
695 ProbeKind::Key { layer } => trace.detail[layer].k = data,
696 ProbeKind::Value { layer } => trace.detail[layer].v = data,
697 ProbeKind::Scores { layer } => trace.detail[layer].scores = data,
698 ProbeKind::AttnHeadOut { layer } => trace.detail[layer].attn_head_out = data,
699 ProbeKind::AttnProjOut { layer } => trace.detail[layer].attn_proj_out = data,
700 ProbeKind::ResidAttn { layer } => trace.detail[layer].resid_attn = data,
701 ProbeKind::Ln2Out { layer } => trace.detail[layer].ln2_out = data,
702 ProbeKind::MlpHidden { layer } => trace.detail[layer].mlp_hidden = data,
703 ProbeKind::BlockOut { layer } => trace.detail[layer].block_out = data,
704 ProbeKind::LnFOut => trace.ln_f_out = data,
705 ProbeKind::Attention { layer } => {
706 let [n_head, q_len, kv_len] = dims[..] else {
707 return Err(ForgeError::Shape(format!(
708 "attention probe expected rank 3, got {dims:?}"
709 )));
710 };
711 trace.attn.push(AttnStep {
712 layer,
713 n_head,
714 q_len,
715 kv_len,
716 probs: data,
717 });
718 }
719 }
720 }
721 Ok((logits, trace))
722 }
723
724 pub fn init_random(config: Gpt2Config, device: &Device, seed: u64) -> Result<Gpt2> {
730 let mut rng = StdRng::seed_from_u64(seed);
731 let c = config.n_embd;
732 let std = 0.02f32;
733 let resid_std = std / (2.0 * config.n_layer as f32).sqrt();
734 let mut normal = |n: usize, std: f32| -> Vec<f32> {
735 (0..n)
736 .map(|_| {
737 let u1: f32 = rng.random::<f32>().max(1e-7);
738 let u2: f32 = rng.random::<f32>();
739 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f32::consts::PI * u2).cos() * std
740 })
741 .collect()
742 };
743 let wte_host = normal(config.vocab_size * c, std);
744 let wpe = Tensor::from_f32(&normal(config.n_ctx * c, std), [config.n_ctx, c], device)?;
745 let emb = Embedding::from_host_wte(&wte_host, config.vocab_size, c, wpe, device)?;
746 drop(wte_host);
747 let eps = config.layer_norm_epsilon;
748 let ones = |dev: &Device| Tensor::from_f32(&vec![1.0f32; c], [c], dev);
749 let zeros1 = |dev: &Device, n: usize| Tensor::zeros([n], dev);
750 let mut blocks = Vec::with_capacity(config.n_layer);
751 for _ in 0..config.n_layer {
752 blocks.push(Block {
753 ln_1: LayerNorm {
754 gamma: ones(device)?,
755 beta: zeros1(device, c)?,
756 eps,
757 },
758 attn_qkv: Linear {
759 w: Tensor::from_f32(&normal(c * 3 * c, std), [c, 3 * c], device)?,
760 b: Some(zeros1(device, 3 * c)?),
761 },
762 attn_proj: Linear {
763 w: Tensor::from_f32(&normal(c * c, resid_std), [c, c], device)?,
764 b: Some(zeros1(device, c)?),
765 },
766 ln_2: LayerNorm {
767 gamma: ones(device)?,
768 beta: zeros1(device, c)?,
769 eps,
770 },
771 mlp_fc: Linear {
772 w: Tensor::from_f32(&normal(c * 4 * c, std), [c, 4 * c], device)?,
773 b: Some(zeros1(device, 4 * c)?),
774 },
775 mlp_proj: Linear {
776 w: Tensor::from_f32(&normal(4 * c * c, resid_std), [4 * c, c], device)?,
777 b: Some(zeros1(device, c)?),
778 },
779 });
780 }
781 let ln_f = LayerNorm {
782 gamma: ones(device)?,
783 beta: zeros1(device, c)?,
784 eps,
785 };
786 Ok(Gpt2 {
787 emb,
788 blocks,
789 ln_f,
790 config,
791 })
792 }
793
794 pub fn param_specs(&self) -> Vec<(String, bool)> {
797 let mut out = vec![
798 ("wte.weight".to_string(), true),
799 ("wpe.weight".to_string(), true),
800 ];
801 for i in 0..self.config.n_layer {
802 out.push((format!("h.{i}.ln_1.weight"), false));
803 out.push((format!("h.{i}.ln_1.bias"), false));
804 out.push((format!("h.{i}.attn.c_attn.weight"), true));
805 out.push((format!("h.{i}.attn.c_attn.bias"), false));
806 out.push((format!("h.{i}.attn.c_proj.weight"), true));
807 out.push((format!("h.{i}.attn.c_proj.bias"), false));
808 out.push((format!("h.{i}.ln_2.weight"), false));
809 out.push((format!("h.{i}.ln_2.bias"), false));
810 out.push((format!("h.{i}.mlp.c_fc.weight"), true));
811 out.push((format!("h.{i}.mlp.c_fc.bias"), false));
812 out.push((format!("h.{i}.mlp.c_proj.weight"), true));
813 out.push((format!("h.{i}.mlp.c_proj.bias"), false));
814 }
815 out.push(("ln_f.weight".to_string(), false));
816 out.push(("ln_f.bias".to_string(), false));
817 out
818 }
819
820 fn wte_single(&self) -> Result<&Tensor> {
822 if self.emb.wte_chunks.len() != 1 {
823 return Err(ForgeError::Shape(format!(
824 "training requires a single-chunk wte; got {} chunks (device \
825 binding limit too small for vocab x n_embd)",
826 self.emb.wte_chunks.len()
827 )));
828 }
829 Ok(&self.emb.wte_chunks[0])
830 }
831
832 pub fn params(&self) -> Result<Vec<&Tensor>> {
834 let mut out = vec![self.wte_single()?, &self.emb.wpe];
835 for b in &self.blocks {
836 out.extend([&b.ln_1.gamma, &b.ln_1.beta, &b.attn_qkv.w]);
837 out.push(bias(&b.attn_qkv)?);
838 out.push(&b.attn_proj.w);
839 out.push(bias(&b.attn_proj)?);
840 out.extend([&b.ln_2.gamma, &b.ln_2.beta, &b.mlp_fc.w]);
841 out.push(bias(&b.mlp_fc)?);
842 out.push(&b.mlp_proj.w);
843 out.push(bias(&b.mlp_proj)?);
844 }
845 out.extend([&self.ln_f.gamma, &self.ln_f.beta]);
846 Ok(out)
847 }
848
849 pub fn params_mut(&mut self) -> Result<Vec<&mut Tensor>> {
851 if self.emb.wte_chunks.len() != 1 {
852 return Err(ForgeError::Shape(
853 "training requires a single-chunk wte".into(),
854 ));
855 }
856 let mut out: Vec<&mut Tensor> = Vec::new();
857 out.push(&mut self.emb.wte_chunks[0]);
858 out.push(&mut self.emb.wpe);
859 for b in &mut self.blocks {
860 out.push(&mut b.ln_1.gamma);
861 out.push(&mut b.ln_1.beta);
862 out.push(&mut b.attn_qkv.w);
863 out.push(
864 b.attn_qkv
865 .b
866 .as_mut()
867 .ok_or_else(|| ForgeError::Shape("training requires biases".into()))?,
868 );
869 out.push(&mut b.attn_proj.w);
870 out.push(
871 b.attn_proj
872 .b
873 .as_mut()
874 .ok_or_else(|| ForgeError::Shape("training requires biases".into()))?,
875 );
876 out.push(&mut b.ln_2.gamma);
877 out.push(&mut b.ln_2.beta);
878 out.push(&mut b.mlp_fc.w);
879 out.push(
880 b.mlp_fc
881 .b
882 .as_mut()
883 .ok_or_else(|| ForgeError::Shape("training requires biases".into()))?,
884 );
885 out.push(&mut b.mlp_proj.w);
886 out.push(
887 b.mlp_proj
888 .b
889 .as_mut()
890 .ok_or_else(|| ForgeError::Shape("training requires biases".into()))?,
891 );
892 }
893 out.push(&mut self.ln_f.gamma);
894 out.push(&mut self.ln_f.beta);
895 Ok(out)
896 }
897
898 pub fn save_safetensors(&self, path: impl AsRef<Path>) -> Result<()> {
901 let c = self.config.n_embd;
902 let mut wte_host = Vec::with_capacity(self.config.vocab_size * c);
903 for ch in &self.emb.wte_chunks {
904 wte_host.extend(ch.to_vec_f32()?);
905 }
906 let mut entries = vec![(
907 "wte.weight".to_string(),
908 vec![self.config.vocab_size, c],
909 wte_host,
910 )];
911 let specs = self.param_specs();
912 let params = self.params_for_save()?;
913 for ((name, _), t) in specs.iter().zip(params).skip(1) {
914 entries.push((name.clone(), t.shape().dims().to_vec(), t.to_vec_f32()?));
915 }
916 crate::serialization::save_safetensors(path, &entries)
917 }
918
919 fn params_for_save(&self) -> Result<Vec<&Tensor>> {
922 let mut out = vec![&self.emb.wte_chunks[0], &self.emb.wpe];
923 for b in &self.blocks {
924 out.extend([&b.ln_1.gamma, &b.ln_1.beta, &b.attn_qkv.w]);
925 out.push(bias(&b.attn_qkv)?);
926 out.push(&b.attn_proj.w);
927 out.push(bias(&b.attn_proj)?);
928 out.extend([&b.ln_2.gamma, &b.ln_2.beta, &b.mlp_fc.w]);
929 out.push(bias(&b.mlp_fc)?);
930 out.push(&b.mlp_proj.w);
931 out.push(bias(&b.mlp_proj)?);
932 }
933 out.extend([&self.ln_f.gamma, &self.ln_f.beta]);
934 Ok(out)
935 }
936
937 pub fn loss(&self, input: &[u32], targets: &[u32]) -> Result<f32> {
942 if input.is_empty() || input.len() != targets.len() {
943 return Err(ForgeError::Shape(
944 "loss needs equal, non-empty input/target lengths".into(),
945 ));
946 }
947 let t = input.len();
948 let device = self.emb.wpe.device();
949 let tgt_t = Tensor::from_u32(targets, [t], &device)?;
950 let logits = self.forward(input)?;
951 let probs = ops::softmax(&logits, false, 0)?;
952 let nll = ops::gather_nll(&probs, &tgt_t)?.to_vec_f32()?;
953 Ok(nll.iter().sum::<f32>() / t as f32)
954 }
955
956 pub fn loss_grads(
961 &self,
962 input: &[u32],
963 targets: &[u32],
964 dropout_p: f32,
965 seed: u32,
966 ) -> Result<(f32, Vec<Tensor>)> {
967 if input.is_empty() || input.len() != targets.len() {
968 return Err(ForgeError::Shape(
969 "loss_grads needs equal, non-empty input/target lengths".into(),
970 ));
971 }
972 if input.len() > self.config.n_ctx {
973 return Err(ForgeError::Shape(format!(
974 "sequence length {} exceeds n_ctx {}",
975 input.len(),
976 self.config.n_ctx
977 )));
978 }
979 let device = self.emb.wpe.device();
980 let t = input.len();
981 let ids_t = Tensor::from_u32(input, [t], &device)?;
982 let tgt_t = Tensor::from_u32(targets, [t], &device)?;
983 let eps = self.config.layer_norm_epsilon;
984 let n_head = self.config.n_head;
985 let hd = self.config.n_embd / n_head;
986
987 let mut tape = Tape::new();
988 let pvars: Vec<TVar> = self
990 .params()?
991 .into_iter()
992 .map(|p| tape.leaf(p.clone()))
993 .collect();
994 let n_params = pvars.len();
995
996 let mut site = 0u32;
997 let mut dseed = move || {
998 site = site.wrapping_add(1);
999 seed.wrapping_mul(0x9E37_79B1)
1000 .wrapping_add(site.wrapping_mul(0x85EB_CA77))
1001 };
1002
1003 let mut x = tape.embedding(&ids_t, &pvars[0], &pvars[1], 0)?;
1004 x = tape.dropout(&x, dropout_p, dseed())?;
1005 for i in 0..self.config.n_layer {
1006 let base = 2 + i * 12;
1007 let [g1, b1, wqkv, bqkv, wproj, bproj, g2, b2, wfc, bfc, wmp, bmp] =
1008 std::array::from_fn(|j| &pvars[base + j]);
1009 let a = tape.layernorm(&x, g1, b1, eps)?;
1010 let qkv = tape.matmul(&a, wqkv, Some(bqkv), MatmulSpec::default())?;
1011 let (q, k, v) = tape.split_heads(&qkv, n_head)?;
1012 let att = tape.matmul(
1013 &q,
1014 &k,
1015 None,
1016 MatmulSpec {
1017 trans_b: true,
1018 alpha: 1.0 / (hd as f32).sqrt(),
1019 ..Default::default()
1020 },
1021 )?;
1022 let probs = tape.softmax(&att, true, 0)?;
1023 let probs = tape.dropout(&probs, dropout_p, dseed())?;
1024 let y = tape.matmul(&probs, &v, None, MatmulSpec::default())?;
1025 let y = tape.merge_heads(&y)?;
1026 let y = tape.matmul(&y, wproj, Some(bproj), MatmulSpec::default())?;
1027 let y = tape.dropout(&y, dropout_p, dseed())?;
1028 x = tape.add(&x, &y)?;
1029 let a2 = tape.layernorm(&x, g2, b2, eps)?;
1030 let f = tape.matmul(&a2, wfc, Some(bfc), MatmulSpec::default())?;
1031 let f = tape.gelu(&f)?;
1032 let f = tape.matmul(&f, wmp, Some(bmp), MatmulSpec::default())?;
1033 let f = tape.dropout(&f, dropout_p, dseed())?;
1034 x = tape.add(&x, &f)?;
1035 }
1036 let xf = tape.layernorm(&x, &pvars[n_params - 2], &pvars[n_params - 1], eps)?;
1037 let logits = tape.matmul(
1040 &xf,
1041 &pvars[0],
1042 None,
1043 MatmulSpec {
1044 trans_b: true,
1045 ..Default::default()
1046 },
1047 )?;
1048
1049 let probs = ops::softmax(&logits.t, false, 0)?;
1050 let nll = ops::gather_nll(&probs, &tgt_t)?.to_vec_f32()?;
1051 let loss = nll.iter().sum::<f32>() / t as f32;
1052 let dlogits = ops::ce_bwd(&probs, &tgt_t, 1.0 / t as f32)?;
1053 let all = tape.backward(&logits, dlogits)?;
1054 let mut grads = Vec::with_capacity(n_params);
1055 for (i, g) in all.into_iter().take(n_params).enumerate() {
1056 grads.push(
1057 g.ok_or_else(|| ForgeError::Shape(format!("parameter {i} received no gradient")))?,
1058 );
1059 }
1060 Ok((loss, grads))
1061 }
1062
1063 pub fn generate(
1066 &self,
1067 tokenizer: &impl Tokenizer,
1068 prompt: &str,
1069 max_new_tokens: usize,
1070 sampling: Sampling,
1071 ) -> Result<String> {
1072 self.generate_streaming(tokenizer, prompt, max_new_tokens, sampling, |_, _| {})
1073 }
1074
1075 pub fn generate_streaming(
1084 &self,
1085 tokenizer: &impl Tokenizer,
1086 prompt: &str,
1087 max_new_tokens: usize,
1088 sampling: Sampling,
1089 mut on_token: impl FnMut(u32, &str),
1090 ) -> Result<String> {
1091 self.generate_streaming_ctl(tokenizer, prompt, max_new_tokens, sampling, |id, text| {
1092 on_token(id, text);
1093 ControlFlow::Continue(())
1094 })
1095 }
1096
1097 pub fn generate_streaming_ctl(
1102 &self,
1103 tokenizer: &impl Tokenizer,
1104 prompt: &str,
1105 max_new_tokens: usize,
1106 sampling: Sampling,
1107 mut on_token: impl FnMut(u32, &str) -> ControlFlow<()>,
1108 ) -> Result<String> {
1109 let mut ids = tokenizer.encode(prompt)?;
1110 if ids.is_empty() {
1111 return Err(ForgeError::Tokenizer("prompt produced no tokens".into()));
1112 }
1113 let mut rng = match sampling {
1114 Sampling::TopK { seed, .. } => Some(StdRng::seed_from_u64(seed)),
1115 Sampling::Greedy => None,
1116 };
1117 let mut cache = self.new_cache()?;
1118 let mut logits = self.logits_step(&ids, &mut cache)?; let mut bytes = tokenizer.decode_bytes(&ids);
1122 let mut sent = emit_valid_prefix(&bytes, 0, &mut |_: &str| {});
1123 for _ in 0..max_new_tokens {
1124 let next = sample(&logits, sampling, rng.as_mut());
1125 if Some(next) == self.config.eos_token_id {
1126 break;
1127 }
1128 ids.push(next);
1129 bytes.extend(tokenizer.decode_bytes(&[next]));
1130 let mut delta = String::new();
1131 sent = emit_valid_prefix(&bytes, sent, &mut |s: &str| delta.push_str(s));
1132 if on_token(next, &delta).is_break() {
1133 break;
1134 }
1135 if ids.len() >= self.config.n_ctx {
1136 break;
1137 }
1138 logits = self.logits_step(&[next], &mut cache)?; }
1140 Ok(tokenizer.decode(&ids))
1141 }
1142
1143 pub async fn generate_async(
1148 &self,
1149 tokenizer: &impl Tokenizer,
1150 prompt: &str,
1151 max_new_tokens: usize,
1152 sampling: Sampling,
1153 mut on_text: impl FnMut(&str),
1154 ) -> Result<String> {
1155 self.generate_async_ctl(tokenizer, prompt, max_new_tokens, sampling, |s| {
1156 on_text(s);
1157 ControlFlow::Continue(())
1158 })
1159 .await
1160 }
1161
1162 pub async fn generate_async_ctl(
1167 &self,
1168 tokenizer: &impl Tokenizer,
1169 prompt: &str,
1170 max_new_tokens: usize,
1171 sampling: Sampling,
1172 on_text: impl FnMut(&str) -> ControlFlow<()>,
1173 ) -> Result<String> {
1174 self.generate_async_probe(
1177 tokenizer,
1178 prompt,
1179 max_new_tokens,
1180 sampling,
1181 on_text,
1182 None::<fn(&[AttnStep])>,
1183 )
1184 .await
1185 }
1186
1187 pub async fn generate_async_probe(
1196 &self,
1197 tokenizer: &impl Tokenizer,
1198 prompt: &str,
1199 max_new_tokens: usize,
1200 sampling: Sampling,
1201 on_text: impl FnMut(&str) -> ControlFlow<()>,
1202 on_attn: Option<impl FnMut(&[AttnStep])>,
1203 ) -> Result<String> {
1204 self.generate_async_trace(
1207 tokenizer,
1208 prompt,
1209 max_new_tokens,
1210 sampling,
1211 on_text,
1212 on_attn.map(|mut f| move |t: &StepTrace| f(&t.attn)),
1213 0,
1214 0,
1215 )
1216 .await
1217 }
1218
1219 #[allow(clippy::too_many_arguments)] pub async fn generate_async_trace(
1230 &self,
1231 tokenizer: &impl Tokenizer,
1232 prompt: &str,
1233 max_new_tokens: usize,
1234 sampling: Sampling,
1235 mut on_text: impl FnMut(&str) -> ControlFlow<()>,
1236 mut on_trace: Option<impl FnMut(&StepTrace)>,
1237 detail_layers: usize,
1238 top_n: usize,
1239 ) -> Result<String> {
1240 let stop = std::cell::Cell::new(false);
1244 let mut on_text = |s: &str| {
1245 if on_text(s).is_break() {
1246 stop.set(true);
1247 }
1248 };
1249 let mut ids = tokenizer.encode(prompt)?;
1250 if ids.is_empty() {
1251 return Err(ForgeError::Tokenizer("prompt produced no tokens".into()));
1252 }
1253 let mut rng = match sampling {
1254 Sampling::TopK { seed, .. } => Some(StdRng::seed_from_u64(seed)),
1255 Sampling::Greedy => None,
1256 };
1257 let mut cache = self.new_cache()?;
1258 let mut logits = self
1259 .step_async(&ids, &mut cache, &mut on_trace, detail_layers, top_n)
1260 .await?; let mut bytes = tokenizer.decode_bytes(&ids);
1265 let mut sent = emit_valid_prefix(&bytes, 0, &mut on_text);
1266 for _ in 0..max_new_tokens {
1267 let next = sample(&logits, sampling, rng.as_mut());
1268 if Some(next) == self.config.eos_token_id {
1269 break;
1270 }
1271 ids.push(next);
1272 bytes.extend(tokenizer.decode_bytes(&[next]));
1273 sent = emit_valid_prefix(&bytes, sent, &mut on_text);
1274 if stop.get() {
1275 break;
1276 }
1277 if ids.len() >= self.config.n_ctx {
1278 break;
1279 }
1280 logits = self
1281 .step_async(&[next], &mut cache, &mut on_trace, detail_layers, top_n)
1282 .await?; }
1284 Ok(tokenizer.decode(&ids))
1285 }
1286
1287 async fn step_async(
1289 &self,
1290 ids: &[u32],
1291 cache: &mut KvCache,
1292 on_trace: &mut Option<impl FnMut(&StepTrace)>,
1293 detail_layers: usize,
1294 top_n: usize,
1295 ) -> Result<Vec<f32>> {
1296 match on_trace {
1297 Some(f) => {
1298 let (logits, trace) = self
1299 .logits_step_trace_async(ids, cache, detail_layers, top_n)
1300 .await?;
1301 f(&trace);
1302 Ok(logits)
1303 }
1304 None => self.logits_step_async(ids, cache).await,
1305 }
1306 }
1307}
1308
1309fn emit_valid_prefix(bytes: &[u8], mut sent: usize, on_text: &mut impl FnMut(&str)) -> usize {
1314 loop {
1315 match std::str::from_utf8(&bytes[sent..]) {
1316 Ok(s) => {
1317 if !s.is_empty() {
1318 on_text(s);
1319 }
1320 return bytes.len();
1321 }
1322 Err(e) => {
1323 let valid = e.valid_up_to();
1324 if valid > 0 {
1325 on_text(std::str::from_utf8(&bytes[sent..sent + valid]).unwrap());
1326 sent += valid;
1327 }
1328 match e.error_len() {
1329 Some(bad) => {
1331 on_text("\u{FFFD}");
1332 sent += bad;
1333 }
1334 None => return sent,
1336 }
1337 }
1338 }
1339 }
1340}
1341
1342fn top_probs(logits: &[f32], n: usize) -> Vec<(u32, f32)> {
1351 let n = n.min(logits.len());
1352 if n == 0 {
1353 return Vec::new();
1354 }
1355 let by_logit = |a: &u32, b: &u32| logits[*b as usize].total_cmp(&logits[*a as usize]);
1356 let mut idx: Vec<u32> = (0..logits.len() as u32).collect();
1357 idx.select_nth_unstable_by(n - 1, by_logit);
1360 idx.truncate(n);
1361 idx.sort_unstable_by(by_logit);
1362
1363 let max = logits.iter().copied().fold(f32::NEG_INFINITY, f32::max);
1364 let total: f32 = logits.iter().map(|l| (l - max).exp()).sum();
1365 idx.into_iter()
1366 .map(|i| (i, (logits[i as usize] - max).exp() / total))
1367 .collect()
1368}
1369
1370fn bias(l: &Linear) -> Result<&Tensor> {
1371 l.b.as_ref()
1372 .ok_or_else(|| ForgeError::Shape("training requires biases".into()))
1373}
1374
1375fn sample(logits: &[f32], sampling: Sampling, rng: Option<&mut StdRng>) -> u32 {
1376 match sampling {
1377 Sampling::Greedy => argmax(logits),
1378 Sampling::TopK { k, temperature, .. } => {
1379 let mut indexed: Vec<(usize, f32)> = logits.iter().copied().enumerate().collect();
1380 indexed.sort_by(|a, b| b.1.total_cmp(&a.1));
1381 indexed.truncate(k.max(1));
1382 let t = temperature.max(1e-4);
1383 let max = indexed[0].1;
1384 let weights: Vec<f32> = indexed.iter().map(|(_, l)| ((l - max) / t).exp()).collect();
1385 let total: f32 = weights.iter().sum();
1386 let mut r = rng.expect("rng required for TopK").random::<f32>() * total;
1387 for ((idx, _), w) in indexed.iter().zip(&weights) {
1388 if r <= *w {
1389 return *idx as u32;
1390 }
1391 r -= w;
1392 }
1393 indexed[0].0 as u32
1394 }
1395 }
1396}
1397
1398fn argmax(v: &[f32]) -> u32 {
1399 let mut best = 0usize;
1400 for (i, &x) in v.iter().enumerate() {
1401 if x > v[best] {
1402 best = i;
1403 }
1404 }
1405 best as u32
1406}