Skip to main content

zsfm_tabpfn/infer/
mod.rs

1//! TabPFN-3 inference on the Burn backend (Flex CPU).
2//!
3//! Feature distribution embedder, column aggregator with RoPE, ICL
4//! transformer with test-only GQA head reduction, ManyClassDecoder readout,
5//! with weights loaded straight from GGUF via [`zsfm_burn::GgufWeights`].
6
7use 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        // x_embed has bias in this checkpoint.
368        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        // y_col_emb [train, D] unsqueezed to [1, train, D] broadcasts over H.
376        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    // Merge batch*heads for the shared 3D sdpa helper.
769    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        // Golden recorded from the verified Burn port (ex-candle parity 8.6e-5).
1132        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}