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