Skip to main content

zsfm_tabfm/
tensor_map.rs

1//! Maps `tabfm/src/pytorch/model.py` state_dict keys (from `torch.load(pytorch_model.bin)`,
2//! after conversion to safetensors) to canonical GGUF tensor names.
3//!
4//! There's no pre-existing GGUF naming convention for this architecture family, so this module
5//! defines one. Stack prefixes mirror the five weight-sharing sub-networks in `TabFM.forward`:
6//!   - `cell`     <- `cell_embedder`            (per-cell Fourier embedding)
7//!   - `colenc1`  <- `col_embedder`             (SetTransformer, stage 1)
8//!   - `colenc2`  <- `col_embedder_2`           (SetTransformer, stage 2)
9//!   - `rowenc1`  <- `row_interactor`           (RoPE cross-column attention, stage 1)
10//!   - `rowenc2`  <- `row_interactor_2`         (RoPE cross-column attention, stage 2)
11//!   - `icl`      <- `icl_predictor`            (24-block in-context-learning attention)
12//! `cls_tokens` is a top-level parameter with no stack.
13//!
14//! Column stacks nest two `MultiheadAttentionBlock`s per transformer block (`mab1`, `mab2` —
15//! induced-attention query/apply pair); row and ICL stacks have one attention+FFN sublayer per
16//! block. Both share the same leaf naming for a block's attention/FFN/norm weights.
17
18pub fn map_tensor_name(hf_name: &str) -> Option<String> {
19    if hf_name == "cls_tokens" {
20        return Some("cls_tokens".into());
21    }
22    if let Some(rest) = hf_name.strip_prefix("cell_embedder.") {
23        return map_cell(rest);
24    }
25    for (prefix, stack) in [
26        ("col_embedder_2.", "colenc2"),
27        ("col_embedder.", "colenc1"),
28        ("row_interactor_2.", "rowenc2"),
29        ("row_interactor.", "rowenc1"),
30    ] {
31        if let Some(rest) = hf_name.strip_prefix(prefix) {
32            return map_stack(stack, rest, prefix.starts_with("col"));
33        }
34    }
35    if let Some(rest) = hf_name.strip_prefix("icl_predictor.") {
36        return map_icl(rest);
37    }
38    None
39}
40
41fn map_cell(rest: &str) -> Option<String> {
42    match rest {
43        "fourier_frequencies" => Some("cell.fourier_freq".into()),
44        "fourier_frequencies_cat" => Some("cell.fourier_freq_cat".into()),
45        "in_linear.weight" => Some("cell.in_linear.weight".into()),
46        "in_linear.bias" => Some("cell.in_linear.bias".into()),
47        "in_linear_cat.weight" => Some("cell.in_linear_cat.weight".into()),
48        "in_linear_cat.bias" => Some("cell.in_linear_cat.bias".into()),
49        // classification: y_embedder_lookup is a plain nn.Embedding
50        "y_embedder_lookup.weight" => Some("cell.y_embed.weight".into()),
51        _ => {
52            // regression: y_embedder_lookup is an MLP (layers.0, layers.1)
53            rest.strip_prefix("y_embedder_lookup.layers.")
54                .and_then(|r| map_mlp_leaf("cell.y_embed", r))
55        }
56    }
57}
58
59/// `rest` is `"{idx}.{weight|bias}"` (a torch `nn.ModuleList` of plain `Linear`s inside an MLP).
60fn map_mlp_leaf(base: &str, rest: &str) -> Option<String> {
61    let dot = rest.find('.')?;
62    let idx: usize = rest[..dot].parse().ok()?;
63    let field = &rest[dot + 1..];
64    if field != "weight" && field != "bias" {
65        return None;
66    }
67    Some(format!("{base}.mlp.{idx}.{field}"))
68}
69
70fn map_stack(stack: &str, rest: &str, is_col: bool) -> Option<String> {
71    if is_col {
72        if let Some(r) = rest.strip_prefix("tf_col.blocks.") {
73            return map_block(stack, r, true);
74        }
75        match rest {
76            "out_w.weight" => Some(format!("{stack}.out_w.weight")),
77            "out_w.bias" => Some(format!("{stack}.out_w.bias")),
78            "ln_w.weight" => Some(format!("{stack}.out_norm.weight")),
79            _ => None,
80        }
81    } else {
82        if rest == "tf_row.rope.freqs" {
83            return Some(format!("{stack}.rope_freqs"));
84        }
85        if let Some(r) = rest.strip_prefix("tf_row.blocks.") {
86            return map_block(stack, r, false);
87        }
88        if rest == "out_ln.weight" {
89            return Some(format!("{stack}.out_norm.weight"));
90        }
91        None
92    }
93}
94
95/// `rest` is `"{block_idx}.{suffix}"`. `has_mab` selects col-stack blocks (two nested
96/// `MultiheadAttentionBlock`s, `mab1`/`mab2`) vs row/ICL blocks (one, unnested).
97fn map_block(stack: &str, rest: &str, has_mab: bool) -> Option<String> {
98    let dot = rest.find('.')?;
99    let n: usize = rest[..dot].parse().ok()?;
100    let suffix = &rest[dot + 1..];
101    let blk_prefix = format!("{stack}.blk.{n}");
102
103    if has_mab {
104        if suffix == "ind_vectors" {
105            return Some(format!("{blk_prefix}.ind_vectors"));
106        }
107        for mab in ["mab1", "mab2"] {
108            if let Some(leaf) = suffix.strip_prefix(&format!("{mab}.")) {
109                let mapped = map_mab_leaf(leaf)?;
110                return Some(format!("{blk_prefix}.{mab}.{mapped}"));
111            }
112        }
113        None
114    } else {
115        let mapped = map_mab_leaf(suffix)?;
116        Some(format!("{blk_prefix}.{mapped}"))
117    }
118}
119
120/// Maps a `MultiheadAttentionBlock`'s direct-child parameter suffix (attention weights, the
121/// four RMSNorms, and the SwiGLU FFN) to its canonical leaf name.
122fn map_mab_leaf(leaf: &str) -> Option<String> {
123    if let Some(r) = leaf.strip_prefix("attn.") {
124        return match r {
125            "q_proj.weight" => Some("attn_q.weight".into()),
126            "q_proj.bias" => Some("attn_q.bias".into()),
127            "k_proj.weight" => Some("attn_k.weight".into()),
128            "k_proj.bias" => Some("attn_k.bias".into()),
129            "v_proj.weight" => Some("attn_v.weight".into()),
130            "v_proj.bias" => Some("attn_v.bias".into()),
131            "out_proj.weight" => Some("attn_o.weight".into()),
132            "out_proj.bias" => Some("attn_o.bias".into()),
133            "query_ln.weight" => Some("q_norm.weight".into()),
134            "key_ln.weight" => Some("k_norm.weight".into()),
135            "per_dim_scale" => Some("per_dim_scale".into()),
136            _ => None,
137        };
138    }
139    match leaf {
140        "pre_attn_ln.weight" => Some("pre_attn_norm.weight".into()),
141        "post_attn_ln.weight" => Some("post_attn_norm.weight".into()),
142        "pre_ff_ln.weight" => Some("pre_ff_norm.weight".into()),
143        "post_ff_ln.weight" => Some("post_ff_norm.weight".into()),
144        "linear1.weight" => Some("ffn_up.weight".into()),
145        "linear1.bias" => Some("ffn_up.bias".into()),
146        "linear1_gate.weight" => Some("ffn_gate.weight".into()),
147        "linear1_gate.bias" => Some("ffn_gate.bias".into()),
148        "linear2.weight" => Some("ffn_down.weight".into()),
149        "linear2.bias" => Some("ffn_down.bias".into()),
150        _ => None,
151    }
152}
153
154fn map_icl(rest: &str) -> Option<String> {
155    if let Some(r) = rest.strip_prefix("tf_icl.blocks.") {
156        return map_block("icl", r, false);
157    }
158    match rest {
159        "ln.weight" => return Some("icl.out_norm.weight".into()),
160        "y_encoder.projection.weight" => return Some("icl.y_encoder.projection.weight".into()),
161        "y_encoder.projection.bias" => return Some("icl.y_encoder.projection.bias".into()),
162        _ => {}
163    }
164    if let Some(r) = rest.strip_prefix("y_encoder.layers.") {
165        return map_mlp_leaf("icl.y_encoder", r);
166    }
167    if let Some(r) = rest.strip_prefix("decoder.layers.") {
168        return map_mlp_leaf("icl.decoder", r);
169    }
170    None
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_cls_tokens() {
179        assert_eq!(map_tensor_name("cls_tokens"), Some("cls_tokens".into()));
180    }
181
182    #[test]
183    fn test_cell_embedder_fourier() {
184        assert_eq!(
185            map_tensor_name("cell_embedder.fourier_frequencies"),
186            Some("cell.fourier_freq".into())
187        );
188        assert_eq!(
189            map_tensor_name("cell_embedder.fourier_frequencies_cat"),
190            Some("cell.fourier_freq_cat".into())
191        );
192    }
193
194    #[test]
195    fn test_cell_embedder_in_linear() {
196        assert_eq!(
197            map_tensor_name("cell_embedder.in_linear.weight"),
198            Some("cell.in_linear.weight".into())
199        );
200        assert_eq!(
201            map_tensor_name("cell_embedder.in_linear_cat.bias"),
202            Some("cell.in_linear_cat.bias".into())
203        );
204    }
205
206    #[test]
207    fn test_cell_embedder_y_embed_classification() {
208        assert_eq!(
209            map_tensor_name("cell_embedder.y_embedder_lookup.weight"),
210            Some("cell.y_embed.weight".into())
211        );
212    }
213
214    #[test]
215    fn test_cell_embedder_y_embed_regression_mlp() {
216        assert_eq!(
217            map_tensor_name("cell_embedder.y_embedder_lookup.layers.0.weight"),
218            Some("cell.y_embed.mlp.0.weight".into())
219        );
220        assert_eq!(
221            map_tensor_name("cell_embedder.y_embedder_lookup.layers.1.bias"),
222            Some("cell.y_embed.mlp.1.bias".into())
223        );
224    }
225
226    #[test]
227    fn test_col_stack_ind_vectors_and_mab() {
228        assert_eq!(
229            map_tensor_name("col_embedder.tf_col.blocks.0.ind_vectors"),
230            Some("colenc1.blk.0.ind_vectors".into())
231        );
232        assert_eq!(
233            map_tensor_name("col_embedder_2.tf_col.blocks.2.mab2.attn.q_proj.weight"),
234            Some("colenc2.blk.2.mab2.attn_q.weight".into())
235        );
236        assert_eq!(
237            map_tensor_name("col_embedder.tf_col.blocks.1.mab1.attn.per_dim_scale"),
238            Some("colenc1.blk.1.mab1.per_dim_scale".into())
239        );
240        assert_eq!(
241            map_tensor_name("col_embedder.tf_col.blocks.0.mab1.pre_ff_ln.weight"),
242            Some("colenc1.blk.0.mab1.pre_ff_norm.weight".into())
243        );
244        assert_eq!(
245            map_tensor_name("col_embedder.tf_col.blocks.0.mab2.linear1_gate.weight"),
246            Some("colenc1.blk.0.mab2.ffn_gate.weight".into())
247        );
248    }
249
250    #[test]
251    fn test_col_stack_out_projection() {
252        assert_eq!(
253            map_tensor_name("col_embedder.out_w.weight"),
254            Some("colenc1.out_w.weight".into())
255        );
256        assert_eq!(
257            map_tensor_name("col_embedder_2.ln_w.weight"),
258            Some("colenc2.out_norm.weight".into())
259        );
260    }
261
262    #[test]
263    fn test_row_stack_rope_and_block() {
264        assert_eq!(
265            map_tensor_name("row_interactor.tf_row.rope.freqs"),
266            Some("rowenc1.rope_freqs".into())
267        );
268        assert_eq!(
269            map_tensor_name("row_interactor_2.tf_row.blocks.2.attn.out_proj.bias"),
270            Some("rowenc2.blk.2.attn_o.bias".into())
271        );
272        assert_eq!(
273            map_tensor_name("row_interactor.tf_row.blocks.0.post_attn_ln.weight"),
274            Some("rowenc1.blk.0.post_attn_norm.weight".into())
275        );
276        assert_eq!(
277            map_tensor_name("row_interactor.out_ln.weight"),
278            Some("rowenc1.out_norm.weight".into())
279        );
280    }
281
282    #[test]
283    fn test_icl_block() {
284        assert_eq!(
285            map_tensor_name("icl_predictor.tf_icl.blocks.23.linear2.weight"),
286            Some("icl.blk.23.ffn_down.weight".into())
287        );
288        assert_eq!(
289            map_tensor_name("icl_predictor.ln.weight"),
290            Some("icl.out_norm.weight".into())
291        );
292    }
293
294    #[test]
295    fn test_icl_y_encoder_classification() {
296        assert_eq!(
297            map_tensor_name("icl_predictor.y_encoder.projection.weight"),
298            Some("icl.y_encoder.projection.weight".into())
299        );
300    }
301
302    #[test]
303    fn test_icl_y_encoder_regression_and_decoder() {
304        assert_eq!(
305            map_tensor_name("icl_predictor.y_encoder.layers.0.weight"),
306            Some("icl.y_encoder.mlp.0.weight".into())
307        );
308        assert_eq!(
309            map_tensor_name("icl_predictor.decoder.layers.1.bias"),
310            Some("icl.decoder.mlp.1.bias".into())
311        );
312    }
313
314    #[test]
315    fn test_unrecognized_returns_none() {
316        assert_eq!(map_tensor_name("some.random.optimizer.state"), None);
317    }
318}