1use std::path::Path;
8
9use anyhow::{Context, Result};
10use burn::tensor::{activation, Device, Tensor, TensorData};
11use zsfm_burn::linear::burn_linear_nd;
12use zsfm_burn::norm::burn_rms_norm_nd;
13use zsfm_burn::{open_gguf_weights, GgufWeights};
14
15use crate::config::TabPfnConfig;
16
17const RMS_EPS: f32 = f32::EPSILON;
18
19fn device() -> Device {
20 Device::flex()
21}
22
23fn load_t(weights: &GgufWeights, name: &str) -> Result<Tensor<2>> {
24 weights.load_2d(name)
25}
26
27fn load_v(weights: &GgufWeights, name: &str) -> Result<Tensor<1>> {
28 weights.load_1d(name)
29}
30
31fn linear_nb<const D: usize>(x: Tensor<D>, w: Tensor<2>) -> Tensor<D> {
32 burn_linear_nd(x, w, None)
33}
34
35fn linear<const D: usize>(x: Tensor<D>, w: Tensor<2>, b: Option<Tensor<1>>) -> Tensor<D> {
36 burn_linear_nd(x, w, b)
37}
38
39fn rms<const D: usize>(x: Tensor<D>, w: Tensor<1>) -> Tensor<D> {
40 burn_rms_norm_nd(x, w, RMS_EPS)
41}
42
43fn gelu<const D: usize>(x: Tensor<D>) -> Tensor<D> {
44 activation::gelu(x)
45}
46
47fn softmax_last<const D: usize>(x: Tensor<D>) -> Tensor<D> {
48 activation::softmax(x, D - 1)
49}
50
51fn to_host<const D: usize>(t: &Tensor<D>) -> Result<Vec<f32>> {
52 t.to_data()
53 .try_to_vec()
54 .map_err(|e| anyhow::anyhow!("{e:?}"))
55}
56
57struct BurnSsmaxW {
58 base_fc1_w: Tensor<2>,
59 base_fc1_b: Tensor<1>,
60 base_fc2_w: Tensor<2>,
61 base_fc2_b: Tensor<1>,
62 query_fc1_w: Tensor<2>,
63 query_fc1_b: Tensor<1>,
64 query_fc2_w: Tensor<2>,
65 query_fc2_b: Tensor<1>,
66}
67
68struct BurnQkvW {
69 q_w: Tensor<2>,
70 k_w: Tensor<2>,
71 v_w: Tensor<2>,
72 out_w: Tensor<2>,
73 ssmax: Option<BurnSsmaxW>,
74}
75
76struct BurnMlpW {
77 fc1_w: Tensor<2>,
78 fc2_w: Tensor<2>,
79}
80
81struct BurnCrossAttnBlockW {
82 attn: BurnQkvW,
83 mlp: BurnMlpW,
84 ln_q_w: Tensor<1>,
85 ln_kv_w: Tensor<1>,
86 ln2_w: Tensor<1>,
87}
88
89struct BurnIsabW {
90 ind_vectors: Tensor<2>,
91 block1: BurnCrossAttnBlockW,
92 block2: BurnCrossAttnBlockW,
93}
94
95struct BurnTransformerBlockW {
96 attn: BurnQkvW,
97 mlp: BurnMlpW,
98 ln_w: Tensor<1>,
99 ln_mlp_w: Tensor<1>,
100}
101
102struct BurnIclBlockW {
103 attn: BurnQkvW,
104 mlp: BurnMlpW,
105 ln_w: Tensor<1>,
106 ln_mlp_w: Tensor<1>,
107}
108
109struct BurnManyClassDecoderW {
110 q_w: Tensor<2>,
111 q_b: Tensor<1>,
112 k_w: Tensor<2>,
113 k_b: Tensor<1>,
114 ssmax: Option<BurnSsmaxW>,
115}
116
117pub struct BurnTabPfnModel {
118 config: TabPfnConfig,
119 x_embed_w: Tensor<2>,
120 x_embed_b: Tensor<1>,
121 col_y_encoder_w: Tensor<2>,
122 icl_y_encoder_w: Tensor<2>,
123 dist_embed: Vec<BurnIsabW>,
124 col_agg_blocks: Vec<BurnTransformerBlockW>,
125 col_agg_cls_tokens: Tensor<2>,
126 col_agg_rope_freqs: Vec<f32>,
127 col_agg_out_ln_w: Tensor<1>,
128 icl_blocks: Vec<BurnIclBlockW>,
129 output_norm_w: Tensor<1>,
130 many_class_decoder: BurnManyClassDecoderW,
131}
132
133fn load_ssmax(weights: &GgufWeights, prefix: &str) -> Result<BurnSsmaxW> {
134 Ok(BurnSsmaxW {
135 base_fc1_w: load_t(weights, &format!("{prefix}.base_mlp.0.weight"))?,
136 base_fc1_b: load_v(weights, &format!("{prefix}.base_mlp.0.bias"))?,
137 base_fc2_w: load_t(weights, &format!("{prefix}.base_mlp.2.weight"))?,
138 base_fc2_b: load_v(weights, &format!("{prefix}.base_mlp.2.bias"))?,
139 query_fc1_w: load_t(weights, &format!("{prefix}.query_mlp.0.weight"))?,
140 query_fc1_b: load_v(weights, &format!("{prefix}.query_mlp.0.bias"))?,
141 query_fc2_w: load_t(weights, &format!("{prefix}.query_mlp.2.weight"))?,
142 query_fc2_b: load_v(weights, &format!("{prefix}.query_mlp.2.bias"))?,
143 })
144}
145
146fn load_qkv(weights: &GgufWeights, prefix: &str, ssmax_prefix: Option<&str>) -> Result<BurnQkvW> {
147 Ok(BurnQkvW {
148 q_w: load_t(weights, &format!("{prefix}.q_projection.weight"))?,
149 k_w: load_t(weights, &format!("{prefix}.k_projection.weight"))?,
150 v_w: load_t(weights, &format!("{prefix}.v_projection.weight"))?,
151 out_w: load_t(weights, &format!("{prefix}.out_projection.weight"))?,
152 ssmax: match ssmax_prefix {
153 Some(p) => Some(load_ssmax(weights, p)?),
154 None => None,
155 },
156 })
157}
158
159fn load_mlp(weights: &GgufWeights, prefix: &str) -> Result<BurnMlpW> {
160 Ok(BurnMlpW {
161 fc1_w: load_t(weights, &format!("{prefix}.0.weight"))?,
162 fc2_w: load_t(weights, &format!("{prefix}.2.weight"))?,
163 })
164}
165
166fn load_cross_attn_block(
167 weights: &GgufWeights,
168 prefix: &str,
169 has_ssmax: bool,
170) -> Result<BurnCrossAttnBlockW> {
171 let ssmax_prefix = format!("{prefix}.attn.softmax_scaling_layer");
172 Ok(BurnCrossAttnBlockW {
173 attn: load_qkv(
174 weights,
175 &format!("{prefix}.attn"),
176 has_ssmax.then_some(ssmax_prefix.as_str()),
177 )?,
178 mlp: load_mlp(weights, &format!("{prefix}.mlp"))?,
179 ln_q_w: load_v(weights, &format!("{prefix}.layernorm_q.weight"))?,
180 ln_kv_w: load_v(weights, &format!("{prefix}.layernorm_kv.weight"))?,
181 ln2_w: load_v(weights, &format!("{prefix}.layernorm2.weight"))?,
182 })
183}
184
185fn load_transformer_block(weights: &GgufWeights, prefix: &str) -> Result<BurnTransformerBlockW> {
186 Ok(BurnTransformerBlockW {
187 attn: load_qkv(weights, &format!("{prefix}.attention"), None)?,
188 mlp: load_mlp(weights, &format!("{prefix}.mlp"))?,
189 ln_w: load_v(weights, &format!("{prefix}.layernorm.weight"))?,
190 ln_mlp_w: load_v(weights, &format!("{prefix}.layernorm_mlp.weight"))?,
191 })
192}
193
194fn load_icl_block(weights: &GgufWeights, prefix: &str) -> Result<BurnIclBlockW> {
195 let ssmax_prefix = format!("{prefix}.icl_attention.softmax_scaling_layer");
196 Ok(BurnIclBlockW {
197 attn: load_qkv(
198 weights,
199 &format!("{prefix}.icl_attention"),
200 Some(&ssmax_prefix),
201 )?,
202 mlp: load_mlp(weights, &format!("{prefix}.mlp"))?,
203 ln_w: load_v(weights, &format!("{prefix}.layernorm.weight"))?,
204 ln_mlp_w: load_v(weights, &format!("{prefix}.layernorm_mlp.weight"))?,
205 })
206}
207
208impl BurnTabPfnModel {
209 pub fn load(gguf_path: &Path, config: TabPfnConfig) -> Result<Self> {
210 let weights = open_gguf_weights(gguf_path)
211 .with_context(|| format!("open {}", gguf_path.display()))?;
212
213 let x_embed_w = load_t(&weights, "x_embed.weight")?;
214 let x_embed_b = load_v(&weights, "x_embed.bias")?;
215 let col_y_encoder_w = load_t(&weights, "col_y_encoder.embedding.weight")?;
216 let icl_y_encoder_w = load_t(&weights, "icl_y_encoder.embedding.weight")?;
217
218 let mut dist_embed = Vec::with_capacity(config.dist_embed_num_blocks);
219 for n in 0..config.dist_embed_num_blocks {
220 let p = format!("feature_distribution_embedder.layers.{n}");
221 dist_embed.push(BurnIsabW {
222 ind_vectors: load_t(&weights, &format!("{p}.inducing_vectors"))?,
223 block1: load_cross_attn_block(&weights, &format!("{p}.cross_attn_block1"), true)?,
224 block2: load_cross_attn_block(&weights, &format!("{p}.cross_attn_block2"), false)?,
225 });
226 }
227
228 let mut col_agg_blocks = Vec::with_capacity(config.feat_agg_num_blocks);
229 for n in 0..config.feat_agg_num_blocks {
230 col_agg_blocks.push(load_transformer_block(
231 &weights,
232 &format!("column_aggregator.blocks.{n}"),
233 )?);
234 }
235 let col_agg_cls_tokens = load_t(&weights, "column_aggregator.cls_tokens")?;
236 let col_agg_out_ln_w = load_v(&weights, "column_aggregator.out_ln.weight")?;
237 let col_agg_rope_freqs: Vec<f32> = weights.load_flat("column_aggregator.rope.freqs")?;
238
239 let mut icl_blocks = Vec::with_capacity(config.nlayers);
240 for n in 0..config.nlayers {
241 icl_blocks.push(load_icl_block(&weights, &format!("icl_blocks.{n}"))?);
242 }
243 let output_norm_w = load_v(&weights, "output_norm.weight")?;
244 let many_class_decoder = BurnManyClassDecoderW {
245 q_w: load_t(&weights, "many_class_decoder.q_projection.weight")?,
246 q_b: load_v(&weights, "many_class_decoder.q_projection.bias")?,
247 k_w: load_t(&weights, "many_class_decoder.k_projection.weight")?,
248 k_b: load_v(&weights, "many_class_decoder.k_projection.bias")?,
249 ssmax: if config.decoder_use_softmax_scaling {
250 Some(load_ssmax(
251 &weights,
252 "many_class_decoder.softmax_scaling_layer",
253 )?)
254 } else {
255 None
256 },
257 };
258
259 Ok(Self {
260 config,
261 x_embed_w,
262 x_embed_b,
263 col_y_encoder_w,
264 icl_y_encoder_w,
265 dist_embed,
266 col_agg_blocks,
267 col_agg_cls_tokens,
268 col_agg_rope_freqs,
269 col_agg_out_ln_w,
270 icl_blocks,
271 output_norm_w,
272 many_class_decoder,
273 })
274 }
275
276 pub fn predict_classification(
277 &self,
278 x_support: &[Vec<f32>],
279 y_support: &[usize],
280 x_query: &[Vec<f32>],
281 n_classes: usize,
282 ) -> Result<Vec<Vec<f32>>> {
283 let dev = device();
284 let train_size = x_support.len();
285 let mut all_rows = x_support.to_vec();
286 all_rows.extend_from_slice(x_query);
287 let t = all_rows.len();
288 let h = all_rows[0].len();
289 let processed = preprocess_x(&all_rows, train_size);
290 let grouped = feature_group_with_nan_indicators(
291 &processed,
292 h,
293 self.config.feature_group_size,
294 self.config.use_nan_indicators,
295 );
296 let cell_dim = grouped[0][0].len();
297 let mut flat = vec![0f32; h * t * cell_dim];
298 for (row_idx, row) in grouped.iter().enumerate() {
299 for (col_idx, cell) in row.iter().enumerate() {
300 let base = col_idx * t * cell_dim + row_idx * cell_dim;
301 flat[base..base + cell_dim].copy_from_slice(cell);
302 }
303 }
304 let x_grouped = Tensor::<3>::from_data(TensorData::new(flat, [h, t, cell_dim]), &dev);
305 let y_col_emb = self.embedding_lookup(y_support, &self.col_y_encoder_w)?;
306 let col_out = self.dist_embed_forward(&x_grouped, &y_col_emb, train_size)?;
307 let row_out = self.col_agg_forward(&col_out, train_size)?;
308 let y_icl_emb = self.embedding_lookup(y_support, &self.icl_y_encoder_w)?;
309 let train_part: Tensor<2> = row_out.clone().narrow(0, 0, train_size);
310 let train_biased = train_part + y_icl_emb;
311 let mut r = if train_size < t {
312 Tensor::cat(
313 vec![train_biased, row_out.narrow(0, train_size, t - train_size)],
314 0,
315 )
316 } else {
317 train_biased
318 };
319 for blk in &self.icl_blocks {
320 r = self.icl_block_forward(&r, blk, train_size)?;
321 }
322 let r = rms(r, self.output_norm_w.clone());
323 let train_emb: Tensor<2> = r.clone().narrow(0, 0, train_size);
324 let test_emb: Tensor<2> = r.narrow(0, train_size, t - train_size);
325 let highest_target = *y_support
326 .iter()
327 .max()
328 .context("y_support must be non-empty")?;
329 let logits = self.many_class_decoder_forward(
330 &train_emb,
331 &test_emb,
332 y_support,
333 highest_target,
334 n_classes,
335 )?;
336 let flat_logits: Vec<f32> = to_host(&logits)?;
337 let n_query = t - train_size;
338 const TEMPERATURE: f32 = 0.9;
339 let mut result = Vec::with_capacity(n_query);
340 for i in 0..n_query {
341 let row = &flat_logits[i * n_classes..(i + 1) * n_classes];
342 let scaled: Vec<f32> = row.iter().map(|&v| v / TEMPERATURE).collect();
343 result.push(zsfm_core::softmax(&scaled));
344 }
345 Ok(result)
346 }
347
348 fn embedding_lookup(&self, y: &[usize], table: &Tensor<2>) -> Result<Tensor<2>> {
349 let dim = table.dims()[1];
350 let flat: Vec<f32> = to_host(table)?;
351 let mut out = vec![0f32; y.len() * dim];
352 for (i, &c) in y.iter().enumerate() {
353 out[i * dim..(i + 1) * dim].copy_from_slice(&flat[c * dim..(c + 1) * dim]);
354 }
355 Ok(Tensor::<2>::from_data(
356 TensorData::new(out, [y.len(), dim]),
357 &device(),
358 ))
359 }
360
361 fn dist_embed_forward(
362 &self,
363 x_grouped: &Tensor<3>,
364 y_col_emb: &Tensor<2>,
365 train_size: usize,
366 ) -> Result<Tensor<3>> {
367 let cell_emb = linear(
369 x_grouped.clone(),
370 self.x_embed_w.clone(),
371 Some(self.x_embed_b.clone()),
372 );
373 let t = cell_emb.dims()[1];
374 let train_part: Tensor<3> = cell_emb.clone().narrow(1, 0, train_size);
375 let yb: Tensor<3> = y_col_emb.clone().unsqueeze_dim(0);
377 let train_biased = train_part + yb;
378 let mut src = if train_size < t {
379 Tensor::cat(
380 vec![train_biased, cell_emb.narrow(1, train_size, t - train_size)],
381 1,
382 )
383 } else {
384 train_biased
385 };
386 let n_heads = self.config.dist_embed_num_heads;
387 let head_dim = self.config.embed_dim / n_heads;
388 for isab in &self.dist_embed {
389 let h = src.dims()[0];
390 let num_inds = isab.ind_vectors.dims()[0];
391 let dim = isab.ind_vectors.dims()[1];
392 let ind: Tensor<3> = isab
393 .ind_vectors
394 .clone()
395 .unsqueeze_dim::<3>(0)
396 .expand([h, num_inds, dim]);
397 let kv_train: Tensor<3> = src.clone().narrow(1, 0, train_size);
398 let hidden = cross_attn_block_forward(
399 &ind,
400 &kv_train,
401 &isab.block1,
402 n_heads,
403 head_dim,
404 train_size,
405 )?;
406 src =
407 cross_attn_block_forward(&src, &hidden, &isab.block2, n_heads, head_dim, num_inds)?;
408 }
409 Ok(src)
410 }
411
412 fn col_agg_forward(&self, col_embeddings: &Tensor<3>, _train_size: usize) -> Result<Tensor<2>> {
413 let cd = col_embeddings.dims();
414 let (h, t) = (cd[0], cd[1]);
415 let dim = self.config.embed_dim;
416 let n_cls = self.config.feat_agg_num_cls_tokens;
417 let n_heads = self.config.feat_agg_num_heads;
418 let head_dim = dim / n_heads;
419 let feat: Tensor<3> = col_embeddings.clone().permute([1, 0, 2]);
420 let cls: Tensor<3> = self
421 .col_agg_cls_tokens
422 .clone()
423 .unsqueeze_dim::<3>(0)
424 .expand([t, n_cls, dim]);
425 let mut seq = Tensor::cat(vec![cls, feat], 1);
426 let max_len = h + n_cls;
427 let (cos, sin) = rope_table(&self.col_agg_rope_freqs, max_len)?;
428 let n_blocks = self.col_agg_blocks.len();
429 for (i, blk) in self.col_agg_blocks.iter().enumerate() {
430 if i + 1 == n_blocks {
431 let cls_q: Tensor<3> = seq.clone().narrow(1, 0, n_cls);
432 seq = transformer_block_forward_cross(
433 &cls_q,
434 &seq,
435 blk,
436 n_heads,
437 head_dim,
438 Some((cos.clone(), sin.clone())),
439 )?;
440 } else {
441 seq = transformer_block_forward(
442 &seq,
443 blk,
444 n_heads,
445 head_dim,
446 Some((cos.clone(), sin.clone())),
447 )?;
448 }
449 }
450 let out = rms(seq, self.col_agg_out_ln_w.clone());
451 Ok(out.reshape([t, n_cls * dim]))
452 }
453
454 fn icl_block_forward(
455 &self,
456 x: &Tensor<2>,
457 blk: &BurnIclBlockW,
458 train_size: usize,
459 ) -> Result<Tensor<2>> {
460 let n_heads = self.config.icl_num_heads;
461 let icl_dim = self.config.icl_dim();
462 let head_dim = icl_dim / n_heads;
463 let t = x.dims()[0];
464 let normed = rms(x.clone(), blk.ln_w.clone());
465 let q = linear_nb(normed.clone(), blk.attn.q_w.clone()).reshape([t, n_heads, head_dim]);
466 let x_train: Tensor<2> = normed.narrow(0, 0, train_size);
467 let k = linear_nb(x_train.clone(), blk.attn.k_w.clone())
468 .reshape([train_size, n_heads, head_dim]);
469 let v = linear_nb(x_train, blk.attn.v_w.clone()).reshape([train_size, n_heads, head_dim]);
470 let q: Tensor<3> = q.permute([1, 0, 2]);
471 let k: Tensor<3> = k.permute([1, 0, 2]);
472 let v: Tensor<3> = v.permute([1, 0, 2]);
473 let attn_out = match self.config.icl_num_kv_heads_test {
474 Some(kv_heads_test) if train_size < t => {
475 let q_train: Tensor<3> = q.clone().narrow(1, 0, train_size);
476 let q_test: Tensor<3> = q.narrow(1, train_size, t - train_size);
477 let out_train = sdpa_with_ssmax(
478 &q_train,
479 &k,
480 &v,
481 blk.attn.ssmax.as_ref(),
482 train_size,
483 n_heads,
484 head_dim,
485 )?;
486 let k_test: Tensor<3> = k.narrow(0, 0, kv_heads_test);
487 let v_test: Tensor<3> = v.narrow(0, 0, kv_heads_test);
488 let k_test = repeat_heads(&k_test, n_heads / kv_heads_test);
489 let v_test = repeat_heads(&v_test, n_heads / kv_heads_test);
490 let out_test = sdpa_with_ssmax(
491 &q_test,
492 &k_test,
493 &v_test,
494 blk.attn.ssmax.as_ref(),
495 train_size,
496 n_heads,
497 head_dim,
498 )?;
499 Tensor::cat(vec![out_train, out_test], 1)
500 }
501 _ => sdpa_with_ssmax(
502 &q,
503 &k,
504 &v,
505 blk.attn.ssmax.as_ref(),
506 train_size,
507 n_heads,
508 head_dim,
509 )?,
510 };
511 let attn_out: Tensor<2> = attn_out.permute([1, 0, 2]).reshape([t, icl_dim]);
512 let attn_out = linear_nb(attn_out, blk.attn.out_w.clone());
513 let x = x.clone() + attn_out;
514 let ff_in = rms(x.clone(), blk.ln_mlp_w.clone());
515 let ff = mlp_forward(ff_in, &blk.mlp);
516 Ok(x + ff)
517 }
518
519 fn many_class_decoder_forward(
520 &self,
521 train_emb: &Tensor<2>,
522 test_emb: &Tensor<2>,
523 y_support: &[usize],
524 highest_target: usize,
525 n_classes: usize,
526 ) -> Result<Tensor<2>> {
527 let dec = &self.many_class_decoder;
528 let n_heads = self.config.decoder_num_heads;
529 let head_dim = self.config.decoder_head_dim;
530 let n = train_emb.dims()[0];
531 let m = test_emb.dims()[0];
532 let q = linear(test_emb.clone(), dec.q_w.clone(), Some(dec.q_b.clone()))
533 .reshape([m, n_heads, head_dim]);
534 let k = linear(train_emb.clone(), dec.k_w.clone(), Some(dec.k_b.clone()))
535 .reshape([n, n_heads, head_dim]);
536 let q: Tensor<3> = q.permute([1, 0, 2]);
537 let k: Tensor<3> = k.permute([1, 0, 2]);
538 let one_hot_width = highest_target + 1;
539 let mut one_hot = vec![0f32; n * one_hot_width];
540 for (i, &c) in y_support.iter().enumerate() {
541 one_hot[i * one_hot_width + c] = 1.0;
542 }
543 let dev = device();
544 let v_row = Tensor::<2>::from_data(TensorData::new(one_hot, [n, one_hot_width]), &dev);
545 let v: Tensor<3> = v_row
546 .unsqueeze_dim::<3>(0)
547 .expand([n_heads, n, one_hot_width]);
548 let num_chunks = one_hot_width.div_ceil(head_dim);
549 let padded_width = num_chunks * head_dim;
550 let v = if padded_width > one_hot_width {
551 let pad = Tensor::<3>::zeros([n_heads, n, padded_width - one_hot_width], &dev);
552 Tensor::cat(vec![v, pad], 2)
553 } else {
554 v
555 };
556 let mut chunk_outs = Vec::with_capacity(num_chunks);
557 for c in 0..num_chunks {
558 let v_chunk: Tensor<3> = v.clone().narrow(2, c * head_dim, head_dim);
559 chunk_outs.push(sdpa_with_ssmax(
560 &q,
561 &k,
562 &v_chunk,
563 dec.ssmax.as_ref(),
564 n,
565 n_heads,
566 head_dim,
567 )?);
568 }
569 let out = if chunk_outs.len() == 1 {
570 chunk_outs.into_iter().next().unwrap()
571 } else {
572 let refs: Vec<Tensor<3>> = chunk_outs;
573 let mut acc = refs[0].clone();
574 for r in refs.iter().skip(1) {
575 acc = Tensor::cat(vec![acc, r.clone()], 2);
576 }
577 acc
578 };
579 let out: Tensor<3> = out.narrow(2, 0, one_hot_width);
580 let out: Tensor<2> = out.mean_dim(0).squeeze_dim(0);
581 let out: Tensor<2> = if n_classes > one_hot_width {
582 let pad = Tensor::<2>::zeros([m, n_classes - one_hot_width], &dev);
583 Tensor::cat(vec![out, pad], 1)
584 } else {
585 out.narrow(1, 0, n_classes)
586 };
587 let clamped = out.clamp(1e-5, f32::INFINITY);
588 Ok((clamped + 3e-5).log())
589 }
590}
591
592fn sdpa_with_ssmax(
593 q: &Tensor<3>,
594 k: &Tensor<3>,
595 v: &Tensor<3>,
596 ssmax: Option<&BurnSsmaxW>,
597 ssmax_n: usize,
598 n_heads: usize,
599 head_dim: usize,
600) -> Result<Tensor<3>> {
601 let q = match ssmax {
602 Some(s) => apply_ssmax(q, s, ssmax_n, n_heads, head_dim)?,
603 None => q.clone(),
604 };
605 let scale = 1.0f32 / (head_dim as f32).sqrt();
606 let scores = q.matmul(k.clone().transpose()).mul_scalar(scale);
607 let probs = softmax_last(scores);
608 Ok(probs.matmul(v.clone()))
609}
610
611fn repeat_heads(x: &Tensor<3>, repeat: usize) -> Tensor<3> {
612 if repeat == 1 {
613 return x.clone();
614 }
615 let d = x.dims();
616 x.clone()
617 .unsqueeze_dim::<4>(1)
618 .expand([d[0], repeat, d[1], d[2]])
619 .reshape([d[0] * repeat, d[1], d[2]])
620}
621
622fn apply_ssmax(
623 q: &Tensor<3>,
624 ssmax: &BurnSsmaxW,
625 n: usize,
626 n_heads: usize,
627 head_dim: usize,
628) -> Result<Tensor<3>> {
629 let dev = device();
630 let logn = (n.max(1) as f32).ln();
631 let logn_t = Tensor::<2>::from_data(TensorData::new(vec![logn], [1, 1]), &dev);
632 let base = gelu(linear(
633 logn_t,
634 ssmax.base_fc1_w.clone(),
635 Some(ssmax.base_fc1_b.clone()),
636 ));
637 let base = linear(
638 base,
639 ssmax.base_fc2_w.clone(),
640 Some(ssmax.base_fc2_b.clone()),
641 );
642 let base: Tensor<3> = base.reshape([n_heads, 1, head_dim]);
643 let qd = q.dims();
644 let (batch_heads, seq) = (qd[0], qd[1]);
645 let batch = batch_heads / n_heads;
646 let base = if batch > 1 {
647 base.unsqueeze_dim::<4>(0)
648 .expand([batch, n_heads, 1, head_dim])
649 .reshape([batch_heads, 1, head_dim])
650 } else {
651 base
652 };
653 let q_flat: Tensor<2> = q.clone().reshape([batch_heads * seq, head_dim]);
654 let qm = gelu(linear(
655 q_flat,
656 ssmax.query_fc1_w.clone(),
657 Some(ssmax.query_fc1_b.clone()),
658 ));
659 let qm = linear(
660 qm,
661 ssmax.query_fc2_w.clone(),
662 Some(ssmax.query_fc2_b.clone()),
663 );
664 let modulation: Tensor<3> = qm
665 .tanh()
666 .add_scalar(1.0)
667 .reshape([batch_heads, seq, head_dim]);
668 let scales = base * modulation;
669 Ok(q.clone() * scales)
670}
671
672fn mlp_forward(x: Tensor<2>, mlp: &BurnMlpW) -> Tensor<2> {
673 let h = gelu(linear_nb(x, mlp.fc1_w.clone()));
674 linear_nb(h, mlp.fc2_w.clone())
675}
676
677fn cross_attn_block_forward(
678 query: &Tensor<3>,
679 context: &Tensor<3>,
680 blk: &BurnCrossAttnBlockW,
681 n_heads: usize,
682 head_dim: usize,
683 ssmax_n: usize,
684) -> Result<Tensor<3>> {
685 let q_normed = rms(query.clone(), blk.ln_q_w.clone());
686 let kv_normed = rms(context.clone(), blk.ln_kv_w.clone());
687 let attn_out = batched_qkv_attention(
688 &q_normed, &kv_normed, &blk.attn, n_heads, head_dim, ssmax_n, None,
689 )?;
690 let x = query.clone() + attn_out;
691 let ff_in = rms(x.clone(), blk.ln2_w.clone());
692 let ff = mlp_forward_2d_to_3d(ff_in, &blk.mlp);
693 Ok(x + ff)
694}
695
696fn mlp_forward_2d_to_3d(x: Tensor<3>, mlp: &BurnMlpW) -> Tensor<3> {
697 let d = x.dims();
698 let h = gelu(linear_nb(x.reshape([d[0] * d[1], d[2]]), mlp.fc1_w.clone()));
699 linear_nb(h, mlp.fc2_w.clone()).reshape(d)
700}
701
702fn transformer_block_forward(
703 x: &Tensor<3>,
704 blk: &BurnTransformerBlockW,
705 n_heads: usize,
706 head_dim: usize,
707 rope_cos_sin: Option<(Tensor<2>, Tensor<2>)>,
708) -> Result<Tensor<3>> {
709 let normed = rms(x.clone(), blk.ln_w.clone());
710 let attn_out = batched_qkv_self_attention(&normed, &blk.attn, n_heads, head_dim, rope_cos_sin)?;
711 let x = x.clone() + attn_out;
712 let ff_in = rms(x.clone(), blk.ln_mlp_w.clone());
713 let ff = mlp_forward_2d_to_3d(ff_in, &blk.mlp);
714 Ok(x + ff)
715}
716
717fn transformer_block_forward_cross(
718 query: &Tensor<3>,
719 context: &Tensor<3>,
720 blk: &BurnTransformerBlockW,
721 n_heads: usize,
722 head_dim: usize,
723 rope_cos_sin: Option<(Tensor<2>, Tensor<2>)>,
724) -> Result<Tensor<3>> {
725 let q_normed = rms(query.clone(), blk.ln_w.clone());
726 let kv_normed = rms(context.clone(), blk.ln_w.clone());
727 let attn_out = batched_qkv_attention(
728 &q_normed,
729 &kv_normed,
730 &blk.attn,
731 n_heads,
732 head_dim,
733 0,
734 rope_cos_sin,
735 )?;
736 let x = query.clone() + attn_out;
737 let ff_in = rms(x.clone(), blk.ln_mlp_w.clone());
738 let ff = mlp_forward_2d_to_3d(ff_in, &blk.mlp);
739 Ok(x + ff)
740}
741
742fn batched_qkv_attention(
743 q_in: &Tensor<3>,
744 kv_in: &Tensor<3>,
745 attn: &BurnQkvW,
746 n_heads: usize,
747 head_dim: usize,
748 ssmax_n: usize,
749 rope_cos_sin: Option<(Tensor<2>, Tensor<2>)>,
750) -> Result<Tensor<3>> {
751 let dim = n_heads * head_dim;
752 let qd = q_in.dims();
753 let (batch, q_len) = (qd[0], qd[1]);
754 let kv_len = kv_in.dims()[1];
755 let q = linear_nb(q_in.clone(), attn.q_w.clone()).reshape([batch, q_len, n_heads, head_dim]);
756 let k = linear_nb(kv_in.clone(), attn.k_w.clone()).reshape([batch, kv_len, n_heads, head_dim]);
757 let v = linear_nb(kv_in.clone(), attn.v_w.clone()).reshape([batch, kv_len, n_heads, head_dim]);
758 let q: Tensor<4> = q.permute([0, 2, 1, 3]);
759 let k: Tensor<4> = k.permute([0, 2, 1, 3]);
760 let v: Tensor<4> = v.permute([0, 2, 1, 3]);
761 let (q, k) = match rope_cos_sin {
762 Some((cos, sin)) => (
763 zsfm_burn::rope::apply_llama_rope(q, cos.clone(), sin.clone(), 0),
764 zsfm_burn::rope::apply_llama_rope(k, cos, sin, 0),
765 ),
766 None => (q, k),
767 };
768 let q: Tensor<3> = q
770 .permute([0, 2, 1, 3])
771 .reshape([batch * n_heads, q_len, head_dim]);
772 let k: Tensor<3> = k
773 .permute([0, 2, 1, 3])
774 .reshape([batch * n_heads, kv_len, head_dim]);
775 let v: Tensor<3> = v
776 .permute([0, 2, 1, 3])
777 .reshape([batch * n_heads, kv_len, head_dim]);
778 let n = if ssmax_n > 0 { ssmax_n } else { kv_len };
779 let out = sdpa_with_ssmax(&q, &k, &v, attn.ssmax.as_ref(), n, n_heads, head_dim)?;
780 let out: Tensor<4> = out
781 .reshape([batch, n_heads, q_len, head_dim])
782 .permute([0, 2, 1, 3]);
783 let out: Tensor<3> = out.reshape([batch, q_len, dim]);
784 Ok(linear_nb(out, attn.out_w.clone()))
785}
786
787fn batched_qkv_self_attention(
788 x: &Tensor<3>,
789 attn: &BurnQkvW,
790 n_heads: usize,
791 head_dim: usize,
792 rope_cos_sin: Option<(Tensor<2>, Tensor<2>)>,
793) -> Result<Tensor<3>> {
794 batched_qkv_attention(x, x, attn, n_heads, head_dim, 0, rope_cos_sin)
795}
796
797fn rope_table(freqs: &[f32], max_len: usize) -> Result<(Tensor<2>, Tensor<2>)> {
798 let dev = device();
799 let half = freqs.len();
800 let mut cos = vec![0f32; max_len * half * 2];
801 let mut sin = vec![0f32; max_len * half * 2];
802 for p in 0..max_len {
803 for i in 0..half {
804 let angle = p as f32 * freqs[i];
805 let (s, c) = angle.sin_cos();
806 cos[p * 2 * half + i] = c;
807 cos[p * 2 * half + half + i] = c;
808 sin[p * 2 * half + i] = s;
809 sin[p * 2 * half + half + i] = s;
810 }
811 }
812 Ok((
813 Tensor::<2>::from_data(TensorData::new(cos, [max_len, 2 * half]), &dev),
814 Tensor::<2>::from_data(TensorData::new(sin, [max_len, 2 * half]), &dev),
815 ))
816}
817
818fn preprocess_x(rows: &[Vec<f32>], train_size: usize) -> Vec<Vec<f32>> {
819 let n_feat = rows[0].len();
820 let mut mean = vec![0f32; n_feat];
821 let mut std = vec![0f32; n_feat];
822 for f in 0..n_feat {
823 let m: f32 = rows[..train_size].iter().map(|r| r[f]).sum::<f32>() / train_size as f32;
824 let denom = if train_size > 1 {
825 (train_size - 1) as f32
826 } else {
827 1.0
828 };
829 let var: f32 = rows[..train_size]
830 .iter()
831 .map(|r| (r[f] - m).powi(2))
832 .sum::<f32>()
833 / denom;
834 mean[f] = m;
835 std[f] = if var == 0.0 || train_size <= 1 {
836 1.0
837 } else {
838 var.sqrt()
839 };
840 }
841 let eps = f32::EPSILON;
842 rows.iter()
843 .map(|row| {
844 row.iter()
845 .enumerate()
846 .map(|(f, &v)| ((v - mean[f]) / (std[f] + eps)).clamp(-100.0, 100.0))
847 .collect()
848 })
849 .collect()
850}
851
852fn feature_group_with_nan_indicators(
853 rows: &[Vec<f32>],
854 h: usize,
855 size: usize,
856 use_nan_indicators: bool,
857) -> Vec<Vec<Vec<f32>>> {
858 rows.iter()
859 .map(|row| {
860 (0..h)
861 .map(|g| {
862 let mut cell: Vec<f32> =
863 (0..size).map(|k| row[(g + (1usize << k)) % h]).collect();
864 if use_nan_indicators {
865 cell.extend(std::iter::repeat_n(0.0f32, size));
866 }
867 cell
868 })
869 .collect()
870 })
871 .collect()
872}
873
874#[cfg(test)]
875mod tests {
876 use super::*;
877 use zsfm_gguf::{GGMLType, GGUFMetaValue, GGUFWriter};
878
879 fn pseudo(n: usize, seed: u64) -> Vec<f32> {
880 let mut x = seed;
881 (0..n)
882 .map(|_| {
883 x = x
884 .wrapping_mul(6364136223846793005)
885 .wrapping_add(1442695040888963407);
886 ((x >> 33) as f32 / u32::MAX as f32) * 2.0 - 1.0
887 })
888 .collect()
889 }
890
891 fn tiny_config() -> TabPfnConfig {
892 TabPfnConfig {
893 embed_dim: 8,
894 dist_embed_num_blocks: 1,
895 dist_embed_num_heads: 2,
896 dist_embed_num_inducing_points: 2,
897 feature_group_size: 2,
898 feat_agg_num_blocks: 1,
899 feat_agg_num_heads: 2,
900 feat_agg_num_cls_tokens: 2,
901 nlayers: 1,
902 icl_num_heads: 2,
903 icl_num_kv_heads_test: Some(1),
904 decoder_head_dim: 4,
905 decoder_num_heads: 2,
906 decoder_use_softmax_scaling: true,
907 ff_factor: 2,
908 softmax_scaling_mlp_hidden_dim: 8,
909 max_num_classes: 2,
910 use_nan_indicators: false,
911 }
912 }
913
914 fn put2(w: &mut GGUFWriter, name: &str, rows: usize, cols: usize, seed: &mut u64, scale: f32) {
915 *seed += 1;
916 let data: Vec<f32> = pseudo(rows * cols, *seed)
917 .iter()
918 .map(|v| v * scale)
919 .collect();
920 let bytes: Vec<u8> = data.iter().flat_map(|v| v.to_le_bytes()).collect();
921 w.add_tensor(name, vec![cols as u64, rows as u64], GGMLType::F32, bytes);
922 }
923
924 fn put1(w: &mut GGUFWriter, name: &str, n: usize, seed: &mut u64) {
925 *seed += 1;
926 let bytes: Vec<u8> = pseudo(n, *seed)
927 .iter()
928 .flat_map(|v| v.to_le_bytes())
929 .collect();
930 w.add_tensor(name, vec![n as u64], GGMLType::F32, bytes);
931 }
932
933 fn put_ssmax(
934 w: &mut GGUFWriter,
935 prefix: &str,
936 base_out: usize,
937 hd: usize,
938 seed: &mut u64,
939 s: f32,
940 ) {
941 put2(w, &format!("{prefix}.base_mlp.0.weight"), 8, 1, seed, s);
942 put1(w, &format!("{prefix}.base_mlp.0.bias"), 8, seed);
943 put2(
944 w,
945 &format!("{prefix}.base_mlp.2.weight"),
946 base_out,
947 8,
948 seed,
949 s,
950 );
951 put1(w, &format!("{prefix}.base_mlp.2.bias"), base_out, seed);
952 put2(w, &format!("{prefix}.query_mlp.0.weight"), 8, hd, seed, s);
953 put1(w, &format!("{prefix}.query_mlp.0.bias"), 8, seed);
954 put2(w, &format!("{prefix}.query_mlp.2.weight"), hd, 8, seed, s);
955 put1(w, &format!("{prefix}.query_mlp.2.bias"), hd, seed);
956 }
957
958 fn put_qkv(
959 w: &mut GGUFWriter,
960 prefix: &str,
961 d: usize,
962 seed: &mut u64,
963 s: f32,
964 ssmax: Option<(usize, usize)>,
965 ) {
966 for t in [
967 "q_projection",
968 "k_projection",
969 "v_projection",
970 "out_projection",
971 ] {
972 put2(w, &format!("{prefix}.{t}.weight"), d, d, seed, s);
973 }
974 if let Some((base_out, hd)) = ssmax {
975 put_ssmax(
976 w,
977 &format!("{prefix}.softmax_scaling_layer"),
978 base_out,
979 hd,
980 seed,
981 s,
982 );
983 }
984 }
985
986 fn put_mlp(w: &mut GGUFWriter, prefix: &str, d: usize, seed: &mut u64, s: f32) {
987 put2(w, &format!("{prefix}.0.weight"), 2 * d, d, seed, s);
988 put2(w, &format!("{prefix}.2.weight"), d, 2 * d, seed, s);
989 }
990
991 #[test]
992 fn burn_classification_golden() {
993 let dir = std::env::temp_dir().join(format!("tabpfn-burn-{}", std::process::id()));
994 let _ = std::fs::remove_dir_all(&dir);
995 std::fs::create_dir_all(&dir).unwrap();
996 let path = dir.join("model.gguf");
997 let mut w = GGUFWriter::new();
998 w.add_metadata(
999 "general.architecture",
1000 GGUFMetaValue::String("tabpfn".into()),
1001 );
1002 let mut seed = 2300u64;
1003 let s = 0.05;
1004 put2(&mut w, "x_embed.weight", 8, 2, &mut seed, s);
1005 put1(&mut w, "x_embed.bias", 8, &mut seed);
1006 put2(&mut w, "col_y_encoder.embedding.weight", 2, 8, &mut seed, s);
1007 put2(
1008 &mut w,
1009 "icl_y_encoder.embedding.weight",
1010 2,
1011 16,
1012 &mut seed,
1013 s,
1014 );
1015 put2(
1016 &mut w,
1017 "feature_distribution_embedder.layers.0.inducing_vectors",
1018 2,
1019 8,
1020 &mut seed,
1021 s,
1022 );
1023 let cb1 = "feature_distribution_embedder.layers.0.cross_attn_block1";
1024 put_qkv(
1025 &mut w,
1026 &format!("{cb1}.attn"),
1027 8,
1028 &mut seed,
1029 s,
1030 Some((8, 4)),
1031 );
1032 put_mlp(&mut w, &format!("{cb1}.mlp"), 8, &mut seed, s);
1033 put1(&mut w, &format!("{cb1}.layernorm_q.weight"), 8, &mut seed);
1034 put1(&mut w, &format!("{cb1}.layernorm_kv.weight"), 8, &mut seed);
1035 put1(&mut w, &format!("{cb1}.layernorm2.weight"), 8, &mut seed);
1036 let cb2 = "feature_distribution_embedder.layers.0.cross_attn_block2";
1037 put_qkv(&mut w, &format!("{cb2}.attn"), 8, &mut seed, s, None);
1038 put_mlp(&mut w, &format!("{cb2}.mlp"), 8, &mut seed, s);
1039 put1(&mut w, &format!("{cb2}.layernorm_q.weight"), 8, &mut seed);
1040 put1(&mut w, &format!("{cb2}.layernorm_kv.weight"), 8, &mut seed);
1041 put1(&mut w, &format!("{cb2}.layernorm2.weight"), 8, &mut seed);
1042 put_qkv(
1043 &mut w,
1044 "column_aggregator.blocks.0.attention",
1045 8,
1046 &mut seed,
1047 s,
1048 None,
1049 );
1050 put_mlp(&mut w, "column_aggregator.blocks.0.mlp", 8, &mut seed, s);
1051 put1(
1052 &mut w,
1053 "column_aggregator.blocks.0.layernorm.weight",
1054 8,
1055 &mut seed,
1056 );
1057 put1(
1058 &mut w,
1059 "column_aggregator.blocks.0.layernorm_mlp.weight",
1060 8,
1061 &mut seed,
1062 );
1063 put2(&mut w, "column_aggregator.cls_tokens", 2, 8, &mut seed, s);
1064 put1(&mut w, "column_aggregator.out_ln.weight", 8, &mut seed);
1065 put1(&mut w, "column_aggregator.rope.freqs", 2, &mut seed);
1066 put_qkv(
1067 &mut w,
1068 "icl_blocks.0.icl_attention",
1069 16,
1070 &mut seed,
1071 s,
1072 Some((16, 8)),
1073 );
1074 put_mlp(&mut w, "icl_blocks.0.mlp", 16, &mut seed, s);
1075 put1(&mut w, "icl_blocks.0.layernorm.weight", 16, &mut seed);
1076 put1(&mut w, "icl_blocks.0.layernorm_mlp.weight", 16, &mut seed);
1077 put1(&mut w, "output_norm.weight", 16, &mut seed);
1078 put2(
1079 &mut w,
1080 "many_class_decoder.q_projection.weight",
1081 8,
1082 16,
1083 &mut seed,
1084 s,
1085 );
1086 put1(&mut w, "many_class_decoder.q_projection.bias", 8, &mut seed);
1087 put2(
1088 &mut w,
1089 "many_class_decoder.k_projection.weight",
1090 8,
1091 16,
1092 &mut seed,
1093 s,
1094 );
1095 put1(&mut w, "many_class_decoder.k_projection.bias", 8, &mut seed);
1096 put_ssmax(
1097 &mut w,
1098 "many_class_decoder.softmax_scaling_layer",
1099 8,
1100 4,
1101 &mut seed,
1102 s,
1103 );
1104 let f = std::fs::File::create(&path).unwrap();
1105 w.write_to(&mut std::io::BufWriter::new(f)).unwrap();
1106
1107 let cfg = tiny_config();
1108 let burn = BurnTabPfnModel::load(&path, cfg).unwrap();
1109 let x_support = vec![
1110 vec![0.1, 1.2],
1111 vec![0.9, -0.3],
1112 vec![-1.1, 0.4],
1113 vec![1.5, 1.1],
1114 ];
1115 let y_support = vec![0usize, 1, 1, 0];
1116 let x_query = vec![vec![0.2, 0.9], vec![-0.8, 0.1]];
1117 let t0 = std::time::Instant::now();
1118 let a = burn
1119 .predict_classification(&x_support, &y_support, &x_query, 2)
1120 .unwrap();
1121 let burn_ms = t0.elapsed().as_secs_f64() * 1000.0;
1122 let b = burn
1123 .predict_classification(&x_support, &y_support, &x_query, 2)
1124 .unwrap();
1125 assert_eq!(a.len(), 2);
1126 assert!(a
1127 .iter()
1128 .all(|r| r.len() == 2 && r.iter().all(|v| v.is_finite())));
1129 assert_eq!(a, b, "predict must be deterministic");
1130 println!("tabpfn synthetic: burn {burn_ms:.1}ms out={a:?}");
1131 let golden = [[0.49975324, 0.50024676], [0.49975356, 0.5002464]];
1133 for (ra, re) in a.iter().zip(golden.iter()) {
1134 for (x, y) in ra.iter().zip(re.iter()) {
1135 assert!((x - y).abs() < 1e-4, "golden drift: {x} vs {y}");
1136 }
1137 }
1138 let _ = std::fs::remove_dir_all(&dir);
1139 }
1140}