1use std::collections::HashMap;
19use std::fmt;
20use std::sync::{Arc, Mutex, OnceLock};
21
22use crate::model::author::NotYet;
23use crate::model::program::{Cut, Identity};
24use crate::model::spec::{Attention, GatedDelta, Mixer, ModelSpec, Rope};
25use crate::tensor_graph::fuse::optimize;
26use crate::tensor_graph::graph::{GraphError, Storage, TensorGraph};
27
28use super::lower_graph::{lower_graph, LowerError};
29use super::shim::EngineShim;
30use super::model::{CacheFormat, EngineNumerics};
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
34pub enum Slot {
35 Residual,
37 Normed,
39 Out,
41 Qkv,
43 Mixed,
45 Z,
47 Alpha,
49 Beta,
50 LogDecay,
52 Decay,
53 Gate,
54 Query,
57 Key,
58 Value,
60 Projected,
62 OutGate,
64 Read,
66 FfnGate,
68 FfnUp,
69 Hidden,
70 Logits,
72}
73
74#[derive(Clone, Debug, PartialEq)]
76pub enum Command {
77 Embed { table: String },
79 Norm { weight: String, from: Slot, to: Slot },
81 Matvec { tensor: String, from: Slot, to: Slot },
83 Add { from: Slot, to: Slot },
85 SwiGlu { gate: Slot, up: Slot, to: Slot },
87 Conv { layer: usize, kernel: String, taps: usize, from: Slot, to: Slot },
90 L2Heads { from: Slot, at: usize, heads: usize, width: usize, to: Slot },
92 Gates { alpha: Slot, beta: Slot, dt: String, a: String, log_decay: Slot, decay: Slot, gate: Slot },
95 Delta {
98 layer: usize,
99 query: Slot,
100 key: Slot,
101 value: Slot,
102 value_at: usize,
103 log_decay: Slot,
104 decay: Slot,
105 gate: Slot,
106 k_heads: usize,
107 v_heads: usize,
108 width: usize,
109 to: Slot,
110 },
111 GatedNorm { weight: String, from: Slot, z: Slot, width: usize, to: Slot },
113 Split { from: Slot, heads: usize, width: usize, query: Slot, gate: Slot },
116 RmsHeads { weight: String, slot: Slot, width: usize },
118 Rotate { slot: Slot, width: usize, rope: Rope },
120 Hadamard { slot: Slot, width: usize },
122 CacheAppend { layer: usize, key: Slot, value: Slot, kv_heads: usize, width: usize, value_width: usize },
125 Attend { layer: usize, query: Slot, heads: usize, kv_heads: usize, width: usize, value_width: usize, scale: f32, to: Slot },
129 SigmoidGate { slot: Slot, gate: Slot },
131 Advance,
133}
134
135#[derive(Clone, Debug, PartialEq)]
137pub struct Program {
138 pub numerics: EngineNumerics,
139 pub commands: Vec<Command>,
140}
141
142#[derive(Clone, Debug, PartialEq, Eq)]
145pub enum ProgramError {
146 NotYet(NotYet),
147 Lift(String),
148 Graph(GraphError),
149 Lower(LowerError),
150}
151
152impl fmt::Display for ProgramError {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 match self {
155 ProgramError::NotYet(e) => write!(f, "{e}"),
156 ProgramError::Lift(e) => write!(f, "the front end does not lift the step: {e}"),
157 ProgramError::Graph(e) => write!(f, "{e}"),
158 ProgramError::Lower(e) => write!(f, "{e}"),
159 }
160 }
161}
162
163impl std::error::Error for ProgramError {}
164
165#[derive(Clone, Debug, PartialEq)]
167pub struct Lowered {
168 pub program: Program,
169 pub identity: Identity,
170}
171
172pub fn lower(spec: &ModelSpec, numerics: EngineNumerics, ctx: usize) -> Result<Lowered, ProgramError> {
176 let (graph, identity) = lifted(spec, numerics, ctx)?;
177 let program = lower_graph(&graph, spec, numerics).map_err(ProgramError::Lower)?;
178 Ok(Lowered { program, identity })
179}
180
181fn lifted(spec: &ModelSpec, numerics: EngineNumerics, ctx: usize) -> Result<(Arc<TensorGraph>, Identity), ProgramError> {
185 static LIFTED: OnceLock<Mutex<HashMap<(String, usize), (Arc<TensorGraph>, Identity)>>> = OnceLock::new();
186 let key = (format!("{spec:?}"), ctx);
187 let cache = LIFTED.get_or_init(Default::default);
188 if let Some((graph, identity)) = cache.lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
189 return Ok((Arc::clone(graph), *identity));
190 }
191 crate::model::author::check(spec).map_err(ProgramError::NotYet)?;
192 let lift = TensorGraph::of_cut(spec, Cut::Decode { ctx }, None, &|_| Storage::Values).map_err(|e| match e {
193 GraphError::Unwalked { why } => ProgramError::Lift(why),
194 e => ProgramError::Graph(e),
195 })?;
196 let identity = lift.identity.expect("a cut's graph records its identity");
197 let graph = Arc::new(optimize(&lift, &EngineShim { spec, numerics }, &[]).graph);
198 cache.lock().unwrap_or_else(|e| e.into_inner()).insert(key, (Arc::clone(&graph), identity));
199 Ok((graph, identity))
200}
201
202pub fn hand_lowering(spec: &ModelSpec, numerics: EngineNumerics) -> Program {
205 let mut c = vec![Command::Embed { table: "token_embd.weight".into() }];
206 for (l, layer) in spec.layers.iter().enumerate() {
207 c.push(Command::Norm { weight: format!("blk.{l}.attn_norm.weight"), from: Slot::Residual, to: Slot::Normed });
208 match &layer.mixer {
209 Mixer::GatedDelta(gd) => delta(&mut c, l, gd),
210 Mixer::Attention(a) => attention(&mut c, l, a, numerics),
211 Mixer::Mla(_) => unreachable!("the engine refuses multi-head latent attention"),
212 }
213 c.push(Command::Add { from: Slot::Out, to: Slot::Residual });
214 c.push(Command::Norm { weight: format!("blk.{l}.{}.weight", spec.ffn_norm), from: Slot::Residual, to: Slot::Normed });
215 c.push(Command::Matvec { tensor: format!("blk.{l}.ffn_gate.weight"), from: Slot::Normed, to: Slot::FfnGate });
216 c.push(Command::Matvec { tensor: format!("blk.{l}.ffn_up.weight"), from: Slot::Normed, to: Slot::FfnUp });
217 c.push(Command::SwiGlu { gate: Slot::FfnGate, up: Slot::FfnUp, to: Slot::Hidden });
218 c.push(Command::Matvec { tensor: format!("blk.{l}.ffn_down.weight"), from: Slot::Hidden, to: Slot::Out });
219 c.push(Command::Add { from: Slot::Out, to: Slot::Residual });
220 }
221 c.push(Command::Advance);
222 c.push(Command::Norm { weight: "output_norm.weight".into(), from: Slot::Residual, to: Slot::Normed });
223 let head = if spec.tied_output { "token_embd.weight" } else { "output.weight" };
224 c.push(Command::Matvec { tensor: head.into(), from: Slot::Normed, to: Slot::Logits });
225 Program { numerics, commands: c }
226}
227
228fn delta(c: &mut Vec<Command>, l: usize, gd: &GatedDelta) {
231 let (hd, nk, nv) = (gd.head_dim, gd.k_heads, gd.v_heads);
232 for (tensor, to) in [("attn_qkv", Slot::Qkv), ("attn_gate", Slot::Z), ("ssm_beta", Slot::Beta), ("ssm_alpha", Slot::Alpha)] {
233 c.push(Command::Matvec { tensor: format!("blk.{l}.{tensor}.weight"), from: Slot::Normed, to });
234 }
235 c.push(Command::Conv { layer: l, kernel: format!("blk.{l}.ssm_conv1d.weight"), taps: gd.conv_taps, from: Slot::Qkv, to: Slot::Mixed });
236 c.push(Command::L2Heads { from: Slot::Mixed, at: 0, heads: nk, width: hd, to: Slot::Query });
237 c.push(Command::L2Heads { from: Slot::Mixed, at: nk * hd, heads: nk, width: hd, to: Slot::Key });
238 c.push(Command::Gates {
239 alpha: Slot::Alpha,
240 beta: Slot::Beta,
241 dt: format!("blk.{l}.ssm_dt.bias"),
242 a: format!("blk.{l}.ssm_a"),
243 log_decay: Slot::LogDecay,
244 decay: Slot::Decay,
245 gate: Slot::Gate,
246 });
247 c.push(Command::Delta {
248 layer: l,
249 query: Slot::Query,
250 key: Slot::Key,
251 value: Slot::Mixed,
252 value_at: 2 * nk * hd,
253 log_decay: Slot::LogDecay,
254 decay: Slot::Decay,
255 gate: Slot::Gate,
256 k_heads: nk,
257 v_heads: nv,
258 width: hd,
259 to: Slot::Read,
260 });
261 c.push(Command::GatedNorm { weight: format!("blk.{l}.ssm_norm.weight"), from: Slot::Read, z: Slot::Z, width: hd, to: Slot::Read });
262 c.push(Command::Matvec { tensor: format!("blk.{l}.ssm_out.weight"), from: Slot::Read, to: Slot::Out });
263}
264
265fn attention(c: &mut Vec<Command>, l: usize, a: &Attention, numerics: EngineNumerics) {
269 let (hd, vd) = (a.head_dim, a.value_dim);
270 let rotated = numerics.cache == CacheFormat::Int8Hadamard;
271 let projected = if a.output_gate { Slot::Projected } else { Slot::Query };
272 c.push(Command::Matvec { tensor: format!("blk.{l}.attn_q.weight"), from: Slot::Normed, to: projected });
273 c.push(Command::Matvec { tensor: format!("blk.{l}.attn_k.weight"), from: Slot::Normed, to: Slot::Key });
274 c.push(Command::Matvec { tensor: format!("blk.{l}.attn_v.weight"), from: Slot::Normed, to: Slot::Value });
275 if a.output_gate {
276 c.push(Command::Split { from: Slot::Projected, heads: a.heads, width: hd, query: Slot::Query, gate: Slot::OutGate });
277 }
278 for (slot, name) in [(Slot::Query, "attn_q_norm"), (Slot::Key, "attn_k_norm")] {
279 if a.qk_norm {
280 c.push(Command::RmsHeads { weight: format!("blk.{l}.{name}.weight"), slot, width: hd });
281 }
282 if let Some(rope) = &a.rope {
283 c.push(Command::Rotate { slot, width: hd, rope: rope.clone() });
284 }
285 }
286 if rotated {
287 c.push(Command::Hadamard { slot: Slot::Key, width: hd });
288 c.push(Command::Hadamard { slot: Slot::Value, width: vd });
289 }
290 c.push(Command::CacheAppend { layer: l, key: Slot::Key, value: Slot::Value, kv_heads: a.kv_heads, width: hd, value_width: vd });
291 if rotated {
292 c.push(Command::Hadamard { slot: Slot::Query, width: hd });
293 }
294 c.push(Command::Attend {
295 layer: l,
296 query: Slot::Query,
297 heads: a.heads,
298 kv_heads: a.kv_heads,
299 width: hd,
300 value_width: vd,
301 scale: a.scale as f32,
302 to: Slot::Read,
303 });
304 if a.output_gate {
305 c.push(Command::SigmoidGate { slot: Slot::Read, gate: Slot::OutGate });
306 }
307 c.push(Command::Matvec { tensor: format!("blk.{l}.attn_output.weight"), from: Slot::Read, to: Slot::Out });
308}
309
310impl Program {
311 pub fn tensors(&self) -> Vec<&str> {
314 let mut seen = std::collections::HashSet::new();
315 let mut out = Vec::new();
316 for c in &self.commands {
317 let names: Vec<&str> = match c {
318 Command::Embed { table } => vec![table.as_str()],
319 Command::Norm { weight, .. } | Command::RmsHeads { weight, .. } | Command::GatedNorm { weight, .. } => vec![weight.as_str()],
320 Command::Matvec { tensor, .. } => vec![tensor.as_str()],
321 Command::Conv { kernel, .. } => vec![kernel.as_str()],
322 Command::Gates { dt, a, .. } => vec![dt.as_str(), a.as_str()],
323 _ => vec![],
324 };
325 for n in names {
326 if seen.insert(n) {
327 out.push(n);
328 }
329 }
330 }
331 out
332 }
333
334 pub fn dataflow(command: &Command) -> (Vec<Slot>, Vec<Slot>) {
336 use Command as C;
337 match command {
338 C::Embed { .. } => (vec![], vec![Slot::Residual]),
339 C::Norm { from, to, .. } | C::Matvec { from, to, .. } | C::Conv { from, to, .. } | C::L2Heads { from, to, .. } => (vec![*from], vec![*to]),
340 C::Add { from, to } => (vec![*from, *to], vec![*to]),
341 C::SwiGlu { gate, up, to } => (vec![*gate, *up], vec![*to]),
342 C::Gates { alpha, beta, log_decay, decay, gate, .. } => (vec![*alpha, *beta], vec![*log_decay, *decay, *gate]),
343 C::Delta { query, key, value, log_decay, decay, gate, to, .. } => (vec![*query, *key, *value, *log_decay, *decay, *gate], vec![*to]),
344 C::GatedNorm { from, z, to, .. } => (vec![*from, *z], vec![*to]),
345 C::Split { from, query, gate, .. } => (vec![*from], vec![*query, *gate]),
346 C::RmsHeads { slot, .. } | C::Rotate { slot, .. } | C::Hadamard { slot, .. } => (vec![*slot], vec![*slot]),
347 C::CacheAppend { key, value, .. } => (vec![*key, *value], vec![]),
348 C::Attend { query, to, .. } => (vec![*query], vec![*to]),
349 C::SigmoidGate { slot, gate } => (vec![*slot, *gate], vec![*slot]),
350 C::Advance => (vec![], vec![]),
351 }
352 }
353}
354
355pub type Word = [u64; 4];
362
363const SLOTS: [Slot; 21] = [
364 Slot::Residual,
365 Slot::Normed,
366 Slot::Out,
367 Slot::Qkv,
368 Slot::Mixed,
369 Slot::Z,
370 Slot::Alpha,
371 Slot::Beta,
372 Slot::LogDecay,
373 Slot::Decay,
374 Slot::Gate,
375 Slot::Query,
376 Slot::Key,
377 Slot::Value,
378 Slot::Projected,
379 Slot::OutGate,
380 Slot::Read,
381 Slot::FfnGate,
382 Slot::FfnUp,
383 Slot::Hidden,
384 Slot::Logits,
385];
386
387#[derive(Clone, Copy, Debug, Default, PartialEq)]
389struct Fields {
390 op: u8,
391 layer: u8,
392 slots: [u8; 7],
393 tensors: [u32; 2],
394 counts: [u16; 4],
395 at: u32,
396 rope: u8,
397 constant: u32,
398}
399
400fn slot_code(s: Slot) -> u8 {
401 s.code()
402}
403
404impl Slot {
405 pub const ALL: [Slot; 21] = SLOTS;
407
408 pub fn code(self) -> u8 {
410 SLOTS.iter().position(|&x| x == self).expect("every slot has a code") as u8
411 }
412
413 pub fn of_code(code: u8) -> Option<Slot> {
415 SLOTS.get(code as usize).copied()
416 }
417}
418
419fn put(w: &mut Word, at: usize, bits: usize, v: u64) {
420 assert!(bits == 64 || v < 1 << bits, "{v} past {bits} bits");
421 for i in 0..bits {
422 if v >> i & 1 == 1 {
423 w[(at + i) / 64] |= 1 << ((at + i) % 64);
424 }
425 }
426}
427
428fn get(w: &Word, at: usize, bits: usize) -> u64 {
429 (0..bits).fold(0, |v, i| v | ((w[(at + i) / 64] >> ((at + i) % 64) & 1) << i))
430}
431
432impl Fields {
433 fn word(&self) -> Word {
434 let mut w = [0u64; 4];
435 put(&mut w, 0, 8, self.op as u64);
436 put(&mut w, 8, 8, self.layer as u64);
437 for (i, &s) in self.slots.iter().enumerate() {
438 put(&mut w, 16 + 5 * i, 5, s as u64);
439 }
440 for (i, &t) in self.tensors.iter().enumerate() {
441 put(&mut w, 51 + 20 * i, 20, t as u64);
442 }
443 for (i, &c) in self.counts.iter().enumerate() {
444 put(&mut w, 91 + 16 * i, 16, c as u64);
445 }
446 put(&mut w, 155, 20, self.at as u64);
447 put(&mut w, 175, 8, self.rope as u64);
448 put(&mut w, 183, 32, self.constant as u64);
449 w
450 }
451
452 fn of(w: &Word) -> Fields {
453 let mut f = Fields { op: get(w, 0, 8) as u8, layer: get(w, 8, 8) as u8, ..Fields::default() };
454 for i in 0..7 {
455 f.slots[i] = get(w, 16 + 5 * i, 5) as u8;
456 }
457 for i in 0..2 {
458 f.tensors[i] = get(w, 51 + 20 * i, 20) as u32;
459 }
460 for i in 0..4 {
461 f.counts[i] = get(w, 91 + 16 * i, 16) as u16;
462 }
463 f.at = get(w, 155, 20) as u32;
464 f.rope = get(w, 175, 8) as u8;
465 f.constant = get(w, 183, 32) as u32;
466 f
467 }
468}
469
470impl Program {
471 pub fn ropes(&self) -> Vec<&Rope> {
473 let mut out: Vec<&Rope> = Vec::new();
474 for c in &self.commands {
475 if let Command::Rotate { rope, .. } = c {
476 if !out.contains(&rope) {
477 out.push(rope);
478 }
479 }
480 }
481 out
482 }
483
484 pub fn encode(&self) -> Vec<Word> {
486 let tensors: std::collections::HashMap<&str, u32> = self.tensors().into_iter().enumerate().map(|(i, n)| (n, i as u32)).collect();
487 let ropes = self.ropes();
488 let t = |name: &str| *tensors.get(name).expect("a tensor the program reads");
489 let s = slot_code;
490 let n16 = |v: usize| u16::try_from(v).expect("a count within 16 bits");
491 let n8 = |v: usize| u8::try_from(v).expect("a layer within 8 bits");
492 self.commands
493 .iter()
494 .map(|c| {
495 let mut f = Fields::default();
496 match c {
497 Command::Embed { table } => (f.op, f.tensors[0]) = (0, t(table)),
498 Command::Norm { weight, from, to } => (f.op, f.tensors[0], f.slots[0], f.slots[1]) = (1, t(weight), s(*from), s(*to)),
499 Command::Matvec { tensor, from, to } => (f.op, f.tensors[0], f.slots[0], f.slots[1]) = (2, t(tensor), s(*from), s(*to)),
500 Command::Add { from, to } => (f.op, f.slots[0], f.slots[1]) = (3, s(*from), s(*to)),
501 Command::SwiGlu { gate, up, to } => (f.op, f.slots[0], f.slots[1], f.slots[2]) = (4, s(*gate), s(*up), s(*to)),
502 Command::Conv { layer, kernel, taps, from, to } => {
503 (f.op, f.layer, f.tensors[0], f.counts[0], f.slots[0], f.slots[1]) = (5, n8(*layer), t(kernel), n16(*taps), s(*from), s(*to))
504 }
505 Command::L2Heads { from, at, heads, width, to } => {
506 (f.op, f.slots[0], f.at, f.counts[0], f.counts[1], f.slots[1]) = (6, s(*from), *at as u32, n16(*heads), n16(*width), s(*to))
507 }
508 Command::Gates { alpha, beta, dt, a, log_decay, decay, gate } => {
509 f.op = 7;
510 f.tensors = [t(dt), t(a)];
511 f.slots[..5].copy_from_slice(&[s(*alpha), s(*beta), s(*log_decay), s(*decay), s(*gate)]);
512 }
513 Command::Delta { layer, query, key, value, value_at, log_decay, decay, gate, k_heads, v_heads, width, to } => {
514 f.op = 8;
515 f.layer = n8(*layer);
516 f.slots = [s(*query), s(*key), s(*value), s(*log_decay), s(*decay), s(*gate), s(*to)];
517 f.at = *value_at as u32;
518 f.counts[..3].copy_from_slice(&[n16(*k_heads), n16(*v_heads), n16(*width)]);
519 }
520 Command::GatedNorm { weight, from, z, width, to } => {
521 (f.op, f.tensors[0], f.slots[0], f.slots[1], f.counts[0], f.slots[2]) = (9, t(weight), s(*from), s(*z), n16(*width), s(*to))
522 }
523 Command::Split { from, heads, width, query, gate } => {
524 (f.op, f.slots[0], f.counts[0], f.counts[1], f.slots[1], f.slots[2]) = (10, s(*from), n16(*heads), n16(*width), s(*query), s(*gate))
525 }
526 Command::RmsHeads { weight, slot, width } => (f.op, f.tensors[0], f.slots[0], f.counts[0]) = (11, t(weight), s(*slot), n16(*width)),
527 Command::Rotate { slot, width, rope } => {
528 let r = ropes.iter().position(|x| *x == rope).expect("a rotary table the program turns by");
529 (f.op, f.slots[0], f.counts[0], f.rope) = (12, s(*slot), n16(*width), r as u8)
530 }
531 Command::Hadamard { slot, width } => (f.op, f.slots[0], f.counts[0]) = (13, s(*slot), n16(*width)),
532 Command::CacheAppend { layer, key, value, kv_heads, width, value_width } => {
533 f.op = 14;
534 f.layer = n8(*layer);
535 f.slots[..2].copy_from_slice(&[s(*key), s(*value)]);
536 f.counts[..3].copy_from_slice(&[n16(*kv_heads), n16(*width), n16(*value_width)]);
537 }
538 Command::Attend { layer, query, heads, kv_heads, width, value_width, scale, to } => {
539 f.op = 15;
540 f.layer = n8(*layer);
541 f.slots[..2].copy_from_slice(&[s(*query), s(*to)]);
542 f.counts = [n16(*heads), n16(*kv_heads), n16(*width), n16(*value_width)];
543 f.constant = scale.to_bits();
544 }
545 Command::SigmoidGate { slot, gate } => (f.op, f.slots[0], f.slots[1]) = (16, s(*slot), s(*gate)),
546 Command::Advance => f.op = 17,
547 }
548 f.word()
549 })
550 .collect()
551 }
552
553 pub fn decode(words: &[Word], tensors: &[&str], ropes: &[&Rope]) -> Vec<Command> {
555 let sl = |c: u8| SLOTS[c as usize];
556 words
557 .iter()
558 .map(|w| {
559 let f = Fields::of(w);
560 let t = |i: usize| tensors[f.tensors[i] as usize].to_string();
561 let [s0, s1, s2, s3, s4, s5, s6] = f.slots.map(sl);
562 let [c0, c1, c2, c3] = f.counts.map(|c| c as usize);
563 let layer = f.layer as usize;
564 match f.op {
565 0 => Command::Embed { table: t(0) },
566 1 => Command::Norm { weight: t(0), from: s0, to: s1 },
567 2 => Command::Matvec { tensor: t(0), from: s0, to: s1 },
568 3 => Command::Add { from: s0, to: s1 },
569 4 => Command::SwiGlu { gate: s0, up: s1, to: s2 },
570 5 => Command::Conv { layer, kernel: t(0), taps: c0, from: s0, to: s1 },
571 6 => Command::L2Heads { from: s0, at: f.at as usize, heads: c0, width: c1, to: s1 },
572 7 => Command::Gates { alpha: s0, beta: s1, dt: t(0), a: t(1), log_decay: s2, decay: s3, gate: s4 },
573 8 => Command::Delta {
574 layer,
575 query: s0,
576 key: s1,
577 value: s2,
578 value_at: f.at as usize,
579 log_decay: s3,
580 decay: s4,
581 gate: s5,
582 k_heads: c0,
583 v_heads: c1,
584 width: c2,
585 to: s6,
586 },
587 9 => Command::GatedNorm { weight: t(0), from: s0, z: s1, width: c0, to: s2 },
588 10 => Command::Split { from: s0, heads: c0, width: c1, query: s1, gate: s2 },
589 11 => Command::RmsHeads { weight: t(0), slot: s0, width: c0 },
590 12 => Command::Rotate { slot: s0, width: c0, rope: ropes[f.rope as usize].clone() },
591 13 => Command::Hadamard { slot: s0, width: c0 },
592 14 => Command::CacheAppend { layer, key: s0, value: s1, kv_heads: c0, width: c1, value_width: c2 },
593 15 => Command::Attend { layer, query: s0, heads: c0, kv_heads: c1, width: c2, value_width: c3, scale: f32::from_bits(f.constant), to: s1 },
594 16 => Command::SigmoidGate { slot: s0, gate: s1 },
595 17 => Command::Advance,
596 op => panic!("no command {op}"),
597 }
598 })
599 .collect()
600 }
601}