spark_model/model/nllb/
mod.rs1use std::collections::HashMap;
20use std::sync::Mutex;
21use std::sync::atomic::{AtomicBool, Ordering};
22
23use anyhow::{Context, Result, ensure};
24use atlas_core::config::ModelConfig;
25use spark_runtime::gpu::{DevicePtr, GpuBackend};
26use spark_runtime::weights::{WeightDtype, WeightStore};
27
28mod beam;
29mod beam_compute;
30mod beam_multi;
31mod compute;
32mod kernels;
33mod kv;
34mod lang;
35mod lora;
36mod model_impl;
37mod util;
38
39pub use lang::NllbLang;
40
41use compute::DecScratch;
42use kernels::NllbKernels;
43use kv::NllbSeqKv;
44use lora::NllbLora;
45
46const DEFAULT_CACHE_ROWS: usize = 512;
50
51pub struct NllbGpuModel {
55 gpu: Box<dyn GpuBackend>,
56 kernels: NllbKernels,
57 weights: HashMap<String, DevicePtr>,
58 embed_table: DevicePtr,
59 d: usize,
61 heads: usize,
62 head_dim: usize,
63 ffn: usize,
64 enc_layers: usize,
65 dec_layers: usize,
66 vocab: usize,
67 embed_scale: f32,
68 attn_scale: f32,
69 cache_rows: usize,
70 max_batch: usize,
71 lang: NllbLang,
72 dec: DecScratch,
74 decode_logits: DevicePtr,
77 prefill_logits: DevicePtr,
80 pos_table: DevicePtr,
82 kv: Mutex<HashMap<usize, NllbSeqKv>>,
84 slots: Mutex<SlotAlloc>,
85 lora: Option<NllbLora>,
87 lora_active: AtomicBool,
91}
92
93#[derive(Default)]
95struct SlotAlloc {
96 next: usize,
97 free: Vec<usize>,
98}
99
100impl SlotAlloc {
101 fn claim(&mut self) -> usize {
102 self.free.pop().unwrap_or_else(|| {
103 let s = self.next;
104 self.next += 1;
105 s
106 })
107 }
108 fn release(&mut self, slot: usize) {
109 self.free.push(slot);
110 }
111}
112
113impl NllbGpuModel {
114 pub fn new(
119 config: &ModelConfig,
120 store: &WeightStore,
121 gpu: Box<dyn GpuBackend>,
122 lang: NllbLang,
123 max_seq_len: usize,
124 max_batch: usize,
125 lora_dir: Option<&std::path::Path>,
126 ) -> Result<Self> {
127 let d = config.hidden_size;
128 let heads = config.num_attention_heads;
129 let head_dim = config.head_dim;
130 let ffn = config.intermediate_size;
131 let dec_layers = config.num_hidden_layers;
132 let enc_layers = dec_layers;
135 let vocab = config.vocab_size;
136 let embed_scale = (d as f32).sqrt();
138 let attn_scale = (head_dim as f32).powf(-0.5);
139 let cache_rows = DEFAULT_CACHE_ROWS.max(max_seq_len.min(2048));
140
141 ensure!(
142 store.get("model.shared.weight")?.dtype == WeightDtype::BF16,
143 "nllb serving requires a bf16 checkpoint; convert with \
144 scripts/convert-safetensors-to-bf16.py"
145 );
146 let weights: HashMap<String, DevicePtr> = store
147 .names()
148 .map(|n| Ok((n.to_string(), store.get(n)?.ptr)))
149 .collect::<Result<_>>()?;
150 let embed_table = *weights
151 .get("model.shared.weight")
152 .context("nllb: missing tied embedding model.shared.weight")?;
153
154 gpu.bind_to_thread()?;
157 let kernels = NllbKernels::new(gpu.as_ref())?;
158 let dec = DecScratch::new(gpu.as_ref(), d, ffn, vocab)?;
159 let max_batch = max_batch.max(1);
160 let decode_logits = gpu.alloc(max_batch * vocab * 2)?;
161 let prefill_logits = gpu.alloc(max_batch * vocab * 2)?;
162 let pos_table = gpu.alloc(cache_rows * d * 2)?;
163 let pos_host = util::decoder_pos_table_bf16(cache_rows, d);
164 gpu.copy_h2d(util::bf16_bytes(&pos_host), pos_table)?;
165
166 let lora = match lora_dir {
167 Some(dir) => Some(NllbLora::load(dir, gpu.as_ref(), cache_rows)?),
168 None => None,
169 };
170
171 tracing::info!(
172 "NLLB served model ready: d={d} heads={heads} enc={enc_layers} dec={dec_layers} \
173 vocab={vocab} src_lang_id={} tgt_lang_id={} cache_rows={cache_rows}",
174 lang.src_lang_id,
175 lang.tgt_lang_id,
176 );
177
178 Ok(Self {
179 gpu,
180 kernels,
181 weights,
182 embed_table,
183 d,
184 heads,
185 head_dim,
186 ffn,
187 enc_layers,
188 dec_layers,
189 vocab,
190 embed_scale,
191 attn_scale,
192 cache_rows,
193 max_batch,
194 lang,
195 dec,
196 decode_logits,
197 prefill_logits,
198 pos_table,
199 kv: Mutex::new(HashMap::new()),
200 slots: Mutex::new(SlotAlloc::default()),
201 lora,
202 lora_active: AtomicBool::new(false),
203 })
204 }
205
206 #[inline]
209 pub(super) fn w(&self, name: &str) -> DevicePtr {
210 self.weights[name]
211 }
212
213 #[inline]
216 pub(super) fn set_lora_active(&self, adapter_slot: i32) {
217 self.lora_active
218 .store(self.lora.is_some() && adapter_slot >= 0, Ordering::Relaxed);
219 }
220
221 #[inline]
222 pub(super) fn lora_is_active(&self) -> bool {
223 self.lora_active.load(Ordering::Relaxed)
224 }
225}