~/satyajit

The tabular foundation model where 98.8% of the weights never see a cell

mdjsonmcp

2026-09-19 · 30 min · explainer · architecture · benchmarks · transformers · open-weights

Tabular data is the last place a transformer was supposed to win. Text has a vocabulary; images have pixels on a grid; audio has samples in time. A table has none of that. It has columns whose meaning lives in a header string the model does not get to read, whose order is arbitrary, whose types are mixed, and whose count changes every time somebody opens a new spreadsheet. There is nothing to pretrain on, because there is no shared input space to pretrain over — my customer_id is your sensor_7, and no amount of data teaches a model what either one means.

That is the real reason gradient-boosted trees have held this ground for fifteen years. A tree does not need a shared input space. It re-derives the whole structure from your data, from scratch, every time.

TabFM, released by Google Research on 30 June 2026, is an answer to that, and it is worth understanding precisely, because the answer is stranger than the announcement makes it sound. The model does not learn a shared vocabulary for columns. It learns to not need one — and then spends 98.8% of its parameters on a transformer that has never seen a cell in its life.

What was actually shipped

That is the first thing to hold onto. Everything below about the architecture I read out of tabfm/src/pytorch/model.py and the safetensors header, because there is no document that states it; every TabFM code block in this piece is quoted from google-research/tabfm at fbb6655, the head of main on the day I wrote this. Everything about the training — the part that carries the entire zero-shot claim — is three sentences in a blog post, and the generator that produced the training data is not in the repository.

To the team's credit, the code they did ship is unusually legible: module names mirror the JAX original for mechanical weight conversion, the comments explain numerical choices ("Normalize entirely in float32… doing the multiply in bf16 loses precision and accumulates across the ~36 RMSNorms per transformer stack"), and the per-fold benchmark results are committed as parquet. That last one matters more than it sounds. It is what let me check their headline against their own numbers, further down, and find that it does not quite hold up.

How a table becomes tokens

Start with the thing the blog post skips. A transformer needs a sequence of vectors. A table is a grid of mixed-typed scalars with no order in either direction. Getting from one to the other is the entire problem, and TabFM does it in four stages with two completely different token grids.

TabFM v1.0.0 · from a spreadsheet to one token per row
0 · OUTSIDE THE MODEL — scikit-learn wrapperordinal by first appearance · mean-impute · z-clip at 4agejobincomey34eng80k051mgr129eng62k044?97k?3 context rows + 1 query row34080000511796672906200044-197000cat_mask = [F, T, F] · d = 3“eng”→0, “mgr”→1, “?”→−1 (unknown and missing share a code)the model never sees NaN: numerics are mean-imputed first,and any NaN that survives is mapped to the sentinel −100.0done 32 times with different column orders, label rotationsand normalisers — that is the “single forward pass”1 · CELL EMBEDDER — 36,032 parameters, the only part that reads a cellx[t,h]x[t,h+1]x[t,h+3]offsets 2^i − 1, taken mod dsin(g·ω), cos(g·ω)ω: 32 learned freqs→ 64 dims eachcats: a 2nd ω setLinear[64→256]sum over the 3cell → 256-dA cell’s embedding is a fixed 3-waycross with columns h+1 and h+3.Move a column and every cell inthe table changes. Nothing here isindexed by name — only by position.label: Embedding[10, 256], contextrows only2 · ALTERNATING ATTENTION — 19.5M parameters total, 1.2% of the modelcol ×3a column as a set of rows256 induced pts · no positionsrow ×3a row across its columnsRoPE over the COLUMN axiscol ×3again, after row mixingmasked to context rowsrow ×3again; keep 8 CLS slotsoutput 8 × 256grid: [T, H, 256]T = every row,context and query3 · ROW COMPRESSION, then 4 · THE 1.62-BILLION-PARAMETER PART8 CLS slots per rowconcat → 2048-dcontext 1context 2context 3query row24 blocks · d = 20488 heads (head dim 256)SwiGLU ff 8192 · no RoPEmask is key-side only: every rowattends to the context rows andnothing else — query rows neversee each other, so this is not transductiveMLP[2048→4096→10]10 logits, hard cap1,619,925,002of the model’s1,639,444,522parameters livein this box, andnone of them hasever seen a cell.Shapes and counts read from classification/config.json and the safetensors header of google/tabfm-1.0.0-pytorch on 2026-09-19;the staging is read from tabfm/src/pytorch/model.py, whose forward() runs cell → col → row → col → row → ICL in that order.

Walk the stages.

Stage 0 happens outside the model. The scikit-learn wrapper detects column types, ordinal-encodes categoricals by order of first appearance (unknown and missing both map to -1), mean-imputes numerics, clips at |z| > 4, and picks a normaliser. By the time anything reaches the network there are no strings, no NaNs, and no column names. The model is handed a float matrix, a boolean mask saying which columns were categorical, and an integer saying how many are real rather than padding. Missing values are not handled by the model at all; they are handled before it. (torch.nan_to_num(x, nan=-100.0) at the entry to forward() is a belt-and-braces sentinel for anything that slips through.)

Stage 1 is the cell embedder, and it is 36,032 parameters. Each cell is grouped with the cells 0, 1 and 3 columns to its right — offsets 2^i - 1, taken modulo the active feature count. Each of the three values is expanded by 32 learned Fourier frequencies into sine/cosine pairs, projected 64 → 256 by one of two linear maps chosen by whether the cell is categorical, and the three are summed. One cell in, one 256-dimensional vector out. For context rows, the label's embedding is added on top.

The grouping is a gather, and it is short enough to read in full:

# tabfm/src/pytorch/model.py — CellEmbedder._group, the three-way column cross
def _group(self, x, d=None):  # x: [B,T,H] -> [B,T,H,G]
    h = x.shape[-1]
    idxs = torch.arange(h, device=x.device)
    stacked = []
    if d is not None:
        d_safe = torch.clamp(d.to(torch.long), min=1)  # [B] active feature count
        for i in range(self.fgs):                      # fgs = feature_group_size = 3
            offset = (2 ** i) - 1                      # 0, 1, 3
            idx = (idxs[None, :] + offset) % d_safe[:, None]         # [B, H]
            idx = idx[:, None, :].expand(x.shape[0], x.shape[1], h)  # [B, T, H]
            stacked.append(torch.gather(x, -1, idx))
    ...
    return torch.stack(stacked, dim=-1)

No column name is read, no column identity is stored. % d_safe is the whole schema-independence trick: a column is an index, the group wraps past the last real column, and d — the count — is the only thing the model knows about your table. Then the expansion, which is where the parameters live:

# the same class — _cell: Fourier features, two heads, sum over the group
def _cell(self, x, cat_mask, d=None):  # [B,t,H] -> [B,t,HC,E]
    g = self._group(x, d=d).unsqueeze(-1).float()
    dt = x.dtype
    ff = self.fourier_frequencies.float()       # [3, 32]
    ffc = self.fourier_frequencies_cat.float()  # [3, 32], a second set for categoricals
    num_out = self.in_linear(torch.cat([(g * ff).sin(), (g * ff).cos()], dim=-1).to(dt))
    if cat_mask is not None:
        cat_out = self.in_linear_cat(torch.cat([(g * ffc).sin(), (g * ffc).cos()], dim=-1).to(dt))
        cmg = self._group(cat_mask[:, None, :].float(), d=d).bool()[..., None]
        return torch.where(cmg, cat_out, num_out).sum(-2)
    return num_out.sum(-2)

That is the whole of TabFM's table-reading machinery: two frequency tables of shape [3, 32], two Linear[64, 256], and a [10, 256] label embedding.

Two things the code says that the prose above does not. The choice between the numeric and categorical heads is a torch.where, not a branch — both projections are computed for every cell, and the categorical one is thrown away on numeric columns. And the Fourier path runs in float32 (the comment in the source says why: the arguments reach ~30, where bf16 sine is not sine). So the smallest module in the model is not the cheapest one to run.

CellEmbedder · 36,032 parameters · one 4,096-row chunk of a 100-column table
THE PARAMETERS — every weight that reads a cellfourier_frequencies[3, 32]96fourier_frequencies_cat[3, 32]96in_linear[64 → 256]16,640in_linear_cat[64 → 256]16,640y_embedder_lookup[10, 256]2,560= 36,0320.0022% of the1,639,444,522THE ACTIVATIONS — what those 36,032 weights push around, per chunkx — the table[1, 4096, 100] · bf160.8 MiB_group → gather ×3[1, 4096, 100, 3, 1] · f324.7 MiBg · ω — 32 freqs[1, 4096, 100, 3, 32] · f32150 MiBcat(sin, cos)[1, 4096, 100, 3, 64] · f32300 MiBin_linear — numeric[1, 4096, 100, 3, 256] · bf16600 MiBin_linear_cat — always[1, 4096, 100, 3, 256] · bf16600 MiBwhere(cat_mask, …)[1, 4096, 100, 3, 256] · bf16600 MiB.sum(-2) → one/cell[1, 4096, 100, 256] · bf16200 MiB1.76 GiBlive at onceBoth projections run for every cell: the categorical head is computed on numeric columns and thrown away by the where.This is why _ROW_CHUNK_SIZE = 4096 exists — the code’s own comment says the expansion “materializes [B,T,HC,G,E]; chunk over rowsso that huge intermediate never exists in full.” A hundred columns is modest; max_num_features caps a member at 500, which is 5× these bars.

Stage 2 alternates column attention and row attention, twice each. For column attention the grid is reshaped so that each column is a sequence of rows — a Set Transformer with 256 induced points, no positional encoding, so it is genuinely permutation-invariant over rows. For row attention the grid is reshaped so that each row is a sequence of columns, and rotary position embeddings run over the column axis. The induced points attend only to context rows, so nothing about the query rows leaks into the column representation.

Stage 3 throws the cell grid away. Eight learned CLS tokens per row survive the second row-attention pass and are concatenated: 8 × 256 = 2048. One vector per row.

Stage 4 is the model. A 24-block transformer, width 2048, 8 heads of dimension 256, SwiGLU feed-forward at 8192, no positional encoding. Every row attends to the labelled context rows and to nothing else — the mask is key-side only, which means query rows do not see each other and predictions are not transductive. A MLP[2048 → 4096 → 10] reads out the logits.

All four stages are ten lines of forward, and the last line is the finding:

# tabfm/src/pytorch/model.py — TabFM.forward, the entire path
def forward(self, x, y, train_size, cat_mask=None, d=None):
    x = torch.nan_to_num(x, nan=-100.0).to(self.cls_tokens.dtype)
    emb = self.cell_embedder(x, y, train_size, cat_mask, d=d)  # [B,T,H,256]
    emb = self.col_embedder(emb, train_size)
    b, t, _, e = emb.shape
    cls = self.cls_tokens.expand(b, t, -1, -1)                 # 8 CLS slots per row
    emb = torch.cat([cls, emb], dim=2)
    emb = self.row_interactor(emb, d=d)
    emb = self.col_embedder_2(emb, train_size)
    reps = self.row_interactor_2(emb, d=d)                     # [B,T,8*256]
    return self.icl_predictor(reps, y, train_size)

icl_predictor is the 1.62 billion. It is handed reps. x — the table — does not appear on that line, and there is no path by which it could: the cell grid was discarded one line earlier.

receiptscaptured 2026-09-19

TabFM v1.0.0 is 1,639,444,522 parameters, and 98.81% of them are in the 24-block transformer that never sees a table cell. The part that actually reads cells — the Fourier cell embedder — is 36,032 parameters, 0.0022% of the model.

stagewhat it attends overparams (clf)shareshape
cell_embedderone cell, plus the cells 1 and 3 columns to its right36,0320.0022%2 x Fourier[3,32] + 2 x Linear[64 -> 256] + Embedding[10,256]
col_embeddera whole column as an unordered set of rows6,581,3760.40%3 induced-attention blocks, d=256, 4 heads, 256 induced points
row_interactorone row across its columns (RoPE over the column axis)3,159,3440.19%3 blocks, d=256, 8 heads, 8 CLS tokens
col_embedder_2the columns again, after row mixing6,581,3760.40%3 induced-attention blocks, d=256
row_interactor_2the row again; keeps only the 8 CLS slots3,159,3440.19%3 blocks, d=256, output 8 x 256 = 2048
cls_tokensthe learned row summary slots2,0480.0001%[8, 256]
icl_predictorone 2048-d vector per row; context rows only1,619,925,00298.81%24 blocks, d=2048, 8 heads (head dim 256), SwiGLU ff 8192
— one ICL blockattention 16.8M + SwiGLU 50.3M67,144,4484.10%x24 = 1,611,466,752
— ICL decoderrow vector to class logits8,433,6740.51%MLP[2048 -> 4096 -> 10]
TOTALclassification checkpoint1,639,444,522100%913 F32 tensors, 6,557,778,088 bytes

Every tensor in both checkpoints is F32, so the classification checkpoint is 6,557,778,088 bytes (6.25 GiB) and the regression checkpoint 6,591,132,852 bytes (6.29 GiB) — 12.5 GiB to hold both, for a model whose inputs are spreadsheets. The loader casts to bfloat16 on load. The regression checkpoint has 1,647,783,213 parameters: the same stack, with an MLP target encoder and a scalar head in place of the 10-way embedding and 10-way decoder.

method Read the safetensors header of google/tabfm-1.0.0-pytorch (both subfolders) over HTTP range requests, summed the product of each tensor's shape, and grouped by top-level module. Shapes cross-checked against classification/config.json and against the module definitions in tabfm/src/pytorch/model.py at google-research/tabfm.
data /articles/tabfm/data/anatomy.json (10 rows, 3.5 KB)

The ratio is the finding. The stack that turns a spreadsheet into row vectors — cell embedder, both column embedders, both row interactors, the CLS tokens — is 19,519,520 parameters. The transformer that reasons over those row vectors is 1,619,925,002. 83 to 1 in favour of the part that never sees a table, and 44,960 to 1 against the cell embedder specifically.

The architecture diagram, and what it leaves out

TabFM architecture diagram: a small table with three feature columns and a label column, two training rows with labels 0 and 1 and a test row with a question mark; an alternating row and column attention block drawn as three rows of three blue nodes with curved arrows running both horizontally within each row and vertically between rows; a row compression arrow from each row into a Row Embedding box; and a stack of three Row Embedding boxes feeding a Predict Missing Label output.
The published architecture figure. Accurate as far as it goes: the three blue nodes per row are cells, and the boxes on the right are the 2048-d row vectors that 98.8% of the parameters operate on (Google Research, TabFM announcement, architecture figure).

The diagram is honest about the shape and silent about the proportions. Three cells, three row embeddings, three boxes — nothing in it suggests that the right third of the picture is 1.62 billion parameters and the left two thirds are 19.5 million. It is also silent about the thirty-two members, about where categoricals are encoded, and about the fact that the left-hand table has already been through a scikit-learn pipeline before it arrives.

The zero-shot claim, mechanically

Here is what "a schema it has never seen" actually means. Nothing in the 1.64 billion parameters is indexed by column identity. No column-name embedding. No per-schema head. No vocabulary. A column is a position, and the only quantity the model knows about the schema is d, the count. That is why an arbitrary table slots in: there is nothing to look up and fail to find.

The elegance has a price, and the price is that column order is now load-bearing twice over. The cell embedder's three-way group is {h, h+1, h+3} mod d, so moving one column rewrites every cell embedding in the table. And the row attention puts RoPE on the column axis, so column position is a coordinate the attention sees directly.

cell_embedder._group · offsets {0, 1, 3} taken mod d · feature_group_size = 3
THE TABLE AS THE USER WROTE ITageh=0jobh=1cityh=2tenureh=3planh=4spendh=5plan wraps past the end: (4+3) mod 6 = 1the six 3-way crosses this ordering builds{age, job, tenure}{job, city, plan}{city, tenure, spend}{tenure, plan, age}{plan, spend, job}{spend, age, city}THE SAME TABLE, AS ENSEMBLE MEMBER #7 SEES ITtenureh=0ageh=1spendh=2cityh=3planh=4jobh=5and the six it builds instead — not one of the originals survives{tenure, age, city}{age, spend, plan}{spend, city, job}{city, plan, tenure}{plan, job, age}{job, tenure, spend}average the logitsn_estimators = 32 is the DEFAULT, not the preset.Each member sees its own column permutation, itsown class-label rotation, and one of two normalisers.The “single forward pass” is thirty-two of them.

So the announcement's own framing — "tables are fundamentally two-dimensional and inherently orderless: swapping two rows or two columns does not change the underlying meaning" — is half true of TabFM. Rows are orderless in the model. Columns are not, and the default estimator spends 32 forward passes washing that out with random permutations.

Which brings us to a phrase worth reading carefully.

Trained on tables that do not exist

The pretraining story is one paragraph in the post and it is the most important paragraph in the release: TabFM is trained entirely on "hundreds of millions of synthetic datasets," generated on the fly from structural causal models with "a wide variety of random functions." No real tables. The stated reason is scarcity — big, diverse, licence-clean tabular corpora essentially do not exist, because the interesting tables are proprietary.

The generator is the evidence for that claim, so: here is the generator.

$ git -C tabfm ls-files 'tabfm/**/*.py' | grep -v _test
tabfm/src/__init__.py
tabfm/src/classifier_and_regressor.py
tabfm/src/hugging_face/convert_and_upload.py
tabfm/src/hugging_face/torch_convert.py
tabfm/src/jax/__init__.py
tabfm/src/jax/checkpointing.py
tabfm/src/jax/memory_efficient_attention.py
tabfm/src/jax/model.py
tabfm/src/jax/tabfm_v1_0_0.py
tabfm/src/pytorch/__init__.py
tabfm/src/pytorch/model.py
tabfm/src/pytorch/tabfm_v1_0_0.py

Twelve files, three of them __init__s: one scikit-learn wrapper, a JAX model and a PyTorch model with their two config modules, a checkpoint loader, an attention kernel, and two Hugging Face converters. No priors/, no data module, no training loop, no sampler. The sentence that carries the entire release ships without the code that would make it checkable.

This is the same bet TabPFN made in 2022 — and TabPFN shipped its prior, so at least the shape of the thing is on record. Its abstract promises one that "entails a large space of structural causal models with a preference for simple structures." In code that is a random MLP with noise at every layer, read rather than evaluated:

# automl/TabPFN at v1.0.0 (a9298ae), tabpfn/priors/mlp.py — get_batch()
def generate_module(layer_idx, out_dim):
    noise = GaussianNoise(...)  # one std per output dim, drawn once at init
    return [nn.Sequential(*[self.prior_mlp_activations(),
                            nn.Linear(self.prior_mlp_hidden_dim, out_dim), noise])]
...
outputs = [causes]
for layer in self.layers:
    outputs.append(layer(outputs[-1]))
outputs = outputs[2:]
 
if self.is_causal:
    ## Sample nodes from graph if model is causal
    outputs_flat = torch.cat(outputs, -1)
    random_perm = torch.randperm(outputs_flat.shape[-1] - 1, device=device)
    random_idx_y = list(range(-num_outputs, -0)) if self.y_is_effect else random_perm[0:num_outputs]
    random_idx = random_perm[num_outputs:num_outputs + num_features]
    y = outputs_flat[:, :, random_idx_y]
    x = outputs_flat[:, :, random_idx]

Read the last four lines. The columns of a synthetic table are not the inputs to a function; they are a random subset of the internals of one, and the label is another node from the same graph. That is what makes the dependence worth learning instead of trivial — the model never sees the causes, only a handful of downstream nodes, which is exactly the situation you are in with a real spreadsheet. Sample a few hundred million of those and a new table's dependence structure is recognisable in-context.

TabFM's generator is presumably bigger and better than this one. Nobody outside Google can say.

That bet has a second payoff the post does not claim, so I will claim it for them: it forecloses benchmark contamination at the root. A model that never saw a real table cannot have seen credit-g. Contrast this with Real-TabPFN, which continues pretraining on curated real OpenML and Kaggle tables and consequently has to build "an enhanced multi-tiered deduplication and filtering pipeline" against every public benchmark to stay honest. TabArena itself is contamination-aware — its leaderboard code carries a warning tag for entrants that involve an LLM, reading "its results depend on a model whose training data we cannot inspect and which may already have seen the test data." TabFM correctly carries no such tag.

That is a real and underrated advantage, and I want it stated plainly before I come back, further down, to what it does not cover.

What the benchmark actually says

Both charts in the announcement are TabArena Elo. Here they are, reproduced from the post:

Two bar charts of TabArena Elo. Top, classification: TabFM-Ensemble 1815, TabFM 1727, AutoGluon 1.5 extreme 4h 1666, TabPFN-3 default 1639, AutoGluon 1.4 extreme 4h 1623, TabPFN-2.6 default 1585, TabICLv2 default 1576, RealTabPFN-2.5 tuned+ensembled 1566, RealMLP tuned+ensembled 1469, TabM tuned+ensembled 1440. Bottom, regression: TabFM-Ensemble 2125, TabFM 1940, TabPFN-3 default 1802, AutoGluon 1.5 1786, RealTabPFN-2.5 1752, TabPFN-2.6 1751, TabDPT 1722, AutoGluon 1.4 1688, TabICLv2 1687, RealMLP 1654.
TabArena Elo for the top ten entrants at release, classification above and regression below. (D) = default, (T+E) = tuned + ensembled. Note what is not in either panel (Google Research, TabFM announcement, results figure).

The post's opening paragraph names AdaBoost, XGBoost and random forests as the incumbents it is displacing. Not one gradient-boosted tree appears in either panel. Ten bars per chart, twenty bars total, zero trees. The nearest thing is AutoGluon, which is an AutoML system that stacks trees among other models. If you want to know where tuned CatBoost sits relative to TabFM you have to go to the leaderboard yourself.

So I did. Here is the live TabArena classification board on the day I wrote this, eleven weeks after release:

TabArena classification Elo · all 38 datasets · read 2026-09-19
LimiX-2 (D)
1926
TabPFN-3.5 (D)
1852
TabPFN-3.5-Fast (D)
1789
Causilo (D)
1776
TabFM (D)
1769
EXAONE-Tabular (D)
1765
Mitra-v2 (D)
1758
TabPFN-3 (D)
1635
TabICLv2 (D)
1577
LightGBM (T+E)
1429
CatBoost (T+E)
1412
CatBoost (D)
1389
XGBoost (T+E)
1375
LightGBM (D)
1191
RandomForest (D)
1000
0500100015002000

Two things fall out of that board that the announcement's chart does not.

The first is the actual gap to the incumbent. Tuned-and-ensembled LightGBM, the strongest tree on the board, sits 340 Elo below TabFM. On TabArena's scale — 400 points is a 91% expected win rate, and default random forest is pinned at exactly 1000 — that is a rout. It is also not a rout against a straw man: "tuned + ensembled" on TabArena means 200 randomly-sampled hyperparameter configurations per model, selected on an inner cross-validation and then post-hoc ensembled, with search spaces taken from the original papers or developed with the models' authors. The denominators hold up.

The second is that TabFM is now fifth. LimiX-2, TabPFN-3.5, TabPFN-3.5-Fast and Causilo have all landed since June. Nothing about the checkpoint changed.

receiptscaptured 2026-09-19

Zero-shot TabFM beats tuned-and-ensembled CatBoost on 90.5% of TabArena folds. It loses to LimiX-2 on 67.5% of them. Same weights, same 51 datasets, same day — the number you quote is a choice of opponent.

opponentclassEloTabFM winsfit s/1Kpredict s/1K
LimiX-2 (default)foundation model197332.5%30.949.028
TabPFN-3.5 (default)foundation model189048.5%1.840.473
TabFM-Ensemble (TabFM+)system184740.3%20.047.629
AutoGluon 1.6 (noncommercial, 4h)system181057.7%14.480.818
TabFM (default) — the subjectfoundation model180316.126.653
AutoGluon 1.5 (extreme, 4h)system167373.7%289.074.031
TabPFN-3 (default)foundation model164978.1%1.290.382
TabPFN-2.6 (default)foundation model159982.9%5.480.555
TabICLv2 (default)foundation model158384.8%0.750.139
TabM (tuned + ensembled)neural net144291.2%2450.132.247
LightGBM (tuned + ensembled)tree142690.7%417.052.639
CatBoost (tuned + ensembled)tree141690.5%1346.210.344
XGBoost (tuned + ensembled)tree136992.4%693.491.689
CatBoost (default)tree1376not published5.880.025
RandomForest (tuned + ensembled)tree116896.0%373.240.771

"Tuned + ensembled" on TabArena means 200 randomly sampled hyperparameter configurations per model, chosen on an inner cross-validation and then post-hoc ensembled, with the search spaces taken from the original papers or developed with the models' authors. These are not straw-man trees. Taking the same board's per-dataset mean errors and comparing pairwise rather than fold-by-fold, TabFM beats CatBoost (T+E) on 48 of the 51 datasets; it loses on hiva_agnostic, Amazon_employee_access and Diabetes130US. What the cost columns show is that the comparison is not like-for-like in the other direction either: CatBoost's 1,346 s/1K is search, paid once; TabFM's 16.1 s/1K is a forward pass over the context, paid on every call, and its 6.65 s/1K prediction is 19x CatBoost's.

method Head-to-head rates are TabArena's own winrate_matrix.csv for all 51 tasks, every entrant, imputation off. Elo, mean rank and the two costs come from the matching website_leaderboard.csv. Costs are median seconds per 1,000 samples; 'fit' is context encoding for a foundation model and hyperparameter search plus training for a tuned baseline.
data /articles/tabfm/data/head-to-head.json (15 rows, 3.7 KB)

The win rates are the number to quote, not the Elo, because Elo is not a property of the model at all.

google/tabfm-1.0.0 · the same weights, six TabArena Elo ratings
Elo (↑) — a rating of this model against whoever else happens to be in the pool262 points of spread · one checkpoint · no retraining17001750180018501900195020001727176917771803194019891727release-day figure · 38 classification datasets1769live board · 38 classification · models only1777live board · 38 classification · every entrant1803live board · all 51 tasks · every entrant1940release-day figure · 13 regression datasets1989live board · 13 regression · models onlyHollow = TabArena live board, read 2026-09-19. Filled = read off the results figure in the Google Research post, 2026-06-30.
receiptscaptured 2026-09-19

The same TabFM weights carry at least six different TabArena Elo ratings, spread over 262 points, because Elo on a living leaderboard is a property of the pool and not of the model. Google's chart quotes the pair from the day of release.

which pooldatasetsTabFM ElopositionTabFM-Ensemble
Google's figure, classification, 30 Jun 20263817272nd of the 10 shown1815 (1st)
Google's figure, regression, 30 Jun 20261319402nd of the 10 shown2125 (1st)
live board, classification, models only3817695th of 84not listed as a model
live board, classification, every entrant3817775th of 861814 (3rd)
live board, regression, models only1319894th of 83not listed as a model
live board, all 51 tasks, every entrant5118037th of 861847 (3rd)

TabArena's own code files TabFM-Ensemble (listed there as TabFM+) under method class 'System'; its constants.py comment reads "Whole pipelines rather than single models: AutoGluon, TabFM+, an agent, a hosted API." Google's chart plots it beside single models. Between release day and this capture LimiX-2, TabPFN-3.5, TabPFN-3.5-Fast, Causilo, EXAONE-Tabular and Mitra-v2 all entered the board; nothing about TabFM changed, and four entrants now sit above it on classification.

method Pulled website_leaderboard.csv from the TabArena leaderboard Space (TabArena/leaderboard) for each entrant set and task slice; read the release-day numbers off the Google Research post's own results figure. Positions are 1-indexed from the CSV's 0-indexed rank column.
data /articles/tabfm/data/denominators.json (6 rows, 2.4 KB)

Same weights, six ratings, 262 points apart. The two Google plots are the two highest numbers available on the day of release. That is not dishonest — every one of these is a correct Elo for its pool — but "1815 versus 1666" reads as a fact about two models and it is a fact about a pool.

One more thing the chart flattens. TabArena's own code files TabFM-Ensemble (which it lists as TabFM+) under method class System, with the comment "Whole pipelines rather than single models: AutoGluon, TabFM+, an agent, a hosted API." The announcement plots it beside single models, in the same chart, as the top bar.

What the ensemble preset is actually buying

The TabFM-Ensemble configuration adds cross features, SVD features, a 32-way non-negative-least-squares blend, and Platt scaling for binary problems. It is worth 88 Elo on the release-day classification chart and 44 on the live all-tasks board. Google committed the per-fold results for both configurations, so this is checkable without running anything.

The preset itself is one classmethod, and the defaults it overrides are the whole story:

# tabfm/src/classifier_and_regressor.py — TabFMClassifier.ensemble()
@classmethod
def ensemble(cls, model: Any, **overrides: Any) -> "TabFMClassifier":
    params = dict(
        n_estimators=32,                         # unchanged — this is also the default
        average_logits=False,                    # default True: average probabilities instead
        n_feature_crosses="sqrt",                # default 0
        n_svd_features="sqrt",                   # default 0
        enable_nnls=True,                        # default False
        binary_calibration_method="platt",       # default None
        multiclass_calibration_method="vector",  # default None
    )
    params.update(overrides)
    return cls(model, **params)

Six flags, and n_estimators is not one of them — the preset does not add a single forward pass to the thirty-two the default already runs. "sqrt" resolves to max(1, int(np.sqrt(n_cols))) over the capped column count, so a 500-column member gets 22 crosses and 22 SVD directions. And it gets them on half the ensemble, because the schedule is deliberately lopsided:

# the same file — EnsembleGenerator._get_member_n_features_list
k_max = self._get_n_features_to_add(n_features_requested, n_cols)
return [0 if i % 2 == 0 else k_max for i in range(self.n_estimators)]

Sixteen augmented views, sixteen plain ones, averaged together. A cross is a product of two numeric columns (X[:, i] * X[:, j], categoricals excluded), and the SVD is fitted on the full uncapped table before per-member subsampling — which is why it rescues hiva_agnostic further down. The blend is non-negative least squares on out-of-fold predictions, then pulled a quarter of the way back to the uniform average by nnls_beta = 0.75. That is not nothing. It is also not a second model.

receiptscaptured 2026-09-19

The 44-Elo gap between TabFM and TabFM-Ensemble is worth a median of half a percent of error on the 30 binary tasks, and concentrates almost entirely in imbalanced and very wide tables. Computed from Google's own committed per-fold results.

slicedatasetsfoldsmetricensemble wins foldswins datasetserror cut
binary30438ROC AUC59.4%21 / 30+0.50%
multiclass8156log loss75.0%8 / 8+1.59%
regression13222RMSE76.6%10 / 13+0.35%
all tasks51816mixed67.0%39 / 51
anneal — imbalance 85:1, 898 rows130log loss+25.2%
APSFailure — 54:1, 170 features19ROC AUC+25.1%
polish_companies_bankruptcy — 13:119ROC AUC+17.3%
hiva_agnostic — 88:1, 1,617 features19log loss+14.5%
churn — the worst single loss19ROC AUC-2.23%

TabArena scores binary tasks with ROC AUC. Platt scaling, which the ensemble preset adds for binary problems, is a monotone map and so cannot change ROC AUC at all — on 30 of the 51 datasets the calibration half of the preset is, by construction, invisible to the score. What is left is the feature crosses, the SVD features and the non-negative-least-squares blend, and their median return is half a percent. TabArena runs 10 repeats of 3-fold CV on datasets up to 2,400 rows and 3 repeats above 2,584, which is why the fold counts differ.

method Read the four parquet files under results/ in google-research/tabfm — 816 (dataset, fold) rows per configuration, JAX on TPU — merged them on (dataset, fold, metric, problem_type) and compared metric_error pairwise. The aggregate rows report the median across datasets of the per-dataset mean relative error change; the single-dataset rows report that dataset's own mean relative change. Nothing here is re-run: these are the numbers Google shipped beside the weights.
data /articles/tabfm/data/ensemble-gain.json (9 rows, 3.1 KB)

Across all 816 folds the ensemble preset wins 67.0% of them. But split it by task and the structure appears: on the 30 binary datasets — 438 folds, scored by ROC AUC — it wins 59.4% of folds, and the median dataset sees its error fall by half a percent. On the 8 multiclass datasets, scored by log loss, it wins 75.0% of folds and the median dataset improves 1.59%. On regression, 76.6% and 0.35%.

There is a mechanical reason the binary column is the weak one, and it is worth spelling out. Platt scaling is a monotone map. ROC AUC depends only on the ranking of the scores. On 30 of the 51 datasets, the calibration half of the "ensemble" preset cannot change the metric by construction. Log loss, which scores the multiclass tasks, is the one metric on TabArena that rewards calibration directly — and that is exactly where the preset earns its keep.

The rest of the gain is concentrated, not broad. The five biggest per-dataset wins are anneal (+25.2%, class imbalance 85:1), APSFailure (+25.1%, 54:1, 170 features), polish_companies_bankruptcy (+17.3%, 13:1), hiva_agnostic (+14.5%, 88:1, 1,617 features) and airfoil_self_noise (+12.6%). Four of the five are imbalanced tables, and three of those are among the five TabArena flags as extremely imbalanced. The honest summary of TabFM-Ensemble is not "pushes performance further"; it is "recovers the calibration and wide-table handling the base model lacks, on the minority of datasets where that is what the metric is measuring."

Where it loses, and why

Taking the leaderboard's per-dataset mean test error, in each dataset's own metric, and comparing it pairwise against tuned-and-ensembled CatBoost: zero-shot TabFM wins 48 of 51. The three it loses:

datasetrowsfeaturesimbalanceTabFM mean rank of 86
hiva_agnostic3,8451,61788:170.3
Amazon_employee_access32,769916:134.8
Diabetes130US71,5184710:119.9

hiva_agnostic is the interesting one, because the mechanism is legible in the code. max_num_features defaults to 500. With 1,617 columns, every one of the 32 ensemble members sees a random 31% slice of the table, and the three-way cell group wraps modulo a different d in each. Mean rank 70.3 of 86 — TabFM's worst result on the suite by a wide margin, worse than default XGBoost. Turn on the ensemble preset, whose SVD features compress all 1,617 columns into dense directions that survive subsampling, and it climbs to 21.6.

Amazon_employee_access is nine columns of high-cardinality identifiers. My reading of TransformToNumerical is that a column arriving as pandas int64 takes the numeric path, which would Fourier-expand raw employee and resource IDs as if they were continuous quantities — the failure mode CatBoost's ordered target statistics exist to prevent. I have not verified the dtype TabArena hands over, so that stays labelled as reasoning, with a falsifier below.

What does not show up is the degradation with dataset size that the "foundation models only win on small data" story predicts. Bucketing the 51 datasets by row count, TabFM's mean rank is 10.96 at ≤10k rows, 11.93 from 10k–100k, and 2.45 on the two datasets above 100k rows. Two datasets is not a trend, but it is not a collapse either.

Where contamination can still get in

Back to the claim above. Direct contamination is ruled out by construction: no real table went into pretraining, so no benchmark table did either. That is the strongest contamination posture any tabular foundation model can have, and it is stronger than the one belonging to every model on the board that fine-tunes on real corpora.

It is also unaudited. The repository contains inference code only. There is no prior, no generator, no training loop, and no report. "Hundreds of millions of synthetic datasets generated using structural causal models" is a sentence, and the property that sentence guarantees is the single load-bearing property of the release. Nobody outside Google can check it.

And there is a second channel, which synthetic pretraining does not close. A prior is designed. So is an architecture, and so are a dozen constants in the wrapper. Every one of those choices was made by people who could read TabArena, and three of them line up with it in ways worth noticing:

None of this is evidence of anything improper. It is the difference between a contamination story that is structurally airtight and one that has actually been audited, and right now TabFM has the first and not the second.

One thing that is checkable, and that speaks well of TabArena: the board re-runs entrants rather than taking submitted numbers. Google's committed parquet results were produced with JAX on TPU; the leaderboard's TabFM row is marked GPU and verified. Comparing the two on all 51 datasets, the median absolute relative difference in mean test error is 0.23%, and 41 of 51 agree within 1%. The exceptions are all datasets where the error is already tiny: APSFailure (0.00739 on TPU against 0.00547 on the board — 35% relative, 0.0019 absolute), anneal (21.6%), polish_companies_bankruptcy (21.2%), and hiva_agnostic, which is the only one with a material absolute gap (0.206 against 0.189). For a model whose compute dtype is bfloat16 and whose default is a 32-member ensemble, that is about the reproducibility you should expect — and it is a reason to read two-significant-figure wins on low-error datasets with some suspicion, whoever is reporting them.

The limits, stated and unstated

The model card is unusually forthcoming, which I want to credit: it lists the 10-class ceiling as a "hard architectural limit", says memory scales with context rows, says behaviour on very wide tables "may degrade", and says outright that "performance on specific real-world domains, minority groups, or edge distributions is not fully characterised." That is a better limitations section than most releases get.

Here is what I would add to it.

The 10-class ceiling is never tested. The decoder's output dimension is literally 10; there is no way around it short of a new checkpoint. TabArena's hardest multiclass task has 8 classes and six of the eight have three. So the ceiling that defines the model's applicability is nowhere near exercised in the reported evaluation.

The README's own limits are wrong. The FAQ says the estimators expose "max_num_features and max_num_rows (defaults are 500 features and 100 context rows)." The 500 is right. max_num_rows defaults to None in all three places it appears — EnsembleGenerator, TabFMClassifier and TabFMRegressor — meaning no cap, every training row goes into the context. Which is the more important fact, because it is where the cost lives.

Regression is a separate model on a third of the evidence. There are two checkpoints, not one. The regression stack swaps the Embedding[10, 256] label lookup for an MLP[1 → 6 → 256] over the scalar target and the 10-way decoder for a 1-way one, which is 1,647,783,213 parameters rather than 1,639,444,522 — and arbitrary target scales are handled outside the model by a StandardScaler on the training targets, inverse-transformed on the way out. TabArena's regression slice is 13 datasets against classification's 38, and it is where TabFM posts its highest Elo. Thirteen datasets is thin ground for the stronger of the two headline numbers.

The cost profile inverts against trees. Median seconds per 1,000 samples on TabArena: TabFM encodes its context in 16.1 and predicts in 6.65; tuned CatBoost burns 1,346 to search and then predicts in 0.344. All in, that is 22.8 against 1,347 — 59× cheaper for a one-shot job you will never repeat. Against a tree you have already fitted, it is 19× more expensive per prediction, forever, and on a GPU rather than a CPU. And there is no fitted artifact — there is a 6.25 GiB float32 checkpoint (12.5 GiB if you want regression too) plus your entire training table, re-encoded on every call unless you opt into cache_context=True.

The licence forbids the obvious use. The code is Apache-2.0. The weights are not.

So what changed

The honest answer to "why did tabular resist foundation models for so long" is that everyone was looking for the wrong shared substrate. There is no universal column vocabulary and there never will be. What TabFM — and TabPFN before it — found is that you do not need one: you need a model that has seen enough shapes of statistical dependence that a new table's dependence structure is recognisable in-context, and you get those shapes from a synthetic prior rather than from data.

What TabFM adds to that idea is scale, and the scale went somewhere specific. TabPFN v2's default classifier checkpoint is 27.7 MiB on the Hub and TabICL's is 103.3 MiB — if those are float32 weights, about 7.3M and 27M parameters. TabFM's is 6.25 GiB and 1,639,444,522 parameters, counted rather than inferred, with 98.8% of it in a transformer reading one token per row. The bet is that in-context learning over compressed rows is the part that benefits from depth, and the table-reading front end can stay tiny. On TabArena, in June, that bet paid: 1727 Elo against 1585 for TabPFN-2.6.

Eleven weeks later there are four models above it, and the two at the top — LimiX-2 at 1926 and TabPFN-3.5 at 1852 — get there at 36.96 and 1.85 median seconds per 1,000 samples against TabFM's 18.45. The category is moving fast enough that the interesting question is no longer whether a transformer can beat a tuned tree on a spreadsheet. It is which of these priors generalises to the table you actually have, and nobody — including Google, which has not released its generator — has published enough to answer that.

What would change my mind

7 claims above, and what would falsify each

  1. TabFM's 'single forward pass' is thirty-two forward passes in the shipped default.

    Read TabFMClassifier.__init__ in tabfm/src/classifier_and_regressor.py: n_estimators: int = 32, with feat_shuffle_method="random", class_shift=True and norm_methods defaulting to ["none", "power"]. If a future release drops the default to 1, or if someone shows the 32 members are fused into one batched call that is meaningfully "a single pass", this framing is wrong. I have measured the architecture, not the wall-clock.

  2. 98.8% of TabFM's parameters are in a stage that never sees a cell.

    Computed from the safetensors header of google/tabfm-1.0.0-pytorch (classification/model.safetensors, 913 F32 tensors) by summing shape products per top-level module: icl_predictor = 1,619,925,002 of 1,639,444,522. Sum it yourself and get a different split, and the claim falls. The word "sees" is doing load-bearing work: the ICL stack reads 2048-d row vectors produced from cells, so it sees cells the way a language model's last layer sees characters.

  3. The binary half of TabArena cannot reward the ensemble preset's calibration step.

    ROC AUC is invariant to any strictly monotone transform of the scores, and Platt scaling is one. If the preset's binary gain came from calibration rather than from crosses, SVD and NNLS blending, that would falsify it — the cleanest test is to run the preset with binary_calibration_method=None and see whether the 30 binary datasets move at all. I have not run it; I inferred it from the metric and the code path.

  4. TabFM's weakness on Amazon_employee_access is high-cardinality categoricals taking the numeric path.

    This is reasoning, not measurement. TransformToNumerical.fit routes any column where pd.api.types.is_numeric_dtype is true to SimpleImputer, not to the ordinal encoder, and nine integer-coded ID columns would qualify. Print clf.X_encoder_.tfm_.transformers_ on that task's DataFrame as TabArena hands it over; if the columns land in the categorical transformer, my explanation is wrong and the loss needs a different one. The loss itself is measured: mean error 0.14196 against CatBoost (T+E) at 0.11760.

  5. Synthetic-only pretraining forecloses direct benchmark contamination, but the claim is unverifiable as shipped.

    The repository contains inference code only — no prior, no generator, no training loop. "Hundreds of millions of synthetic datasets from structural causal models" is a sentence in a blog post. Releasing the generator, or a technical report describing it, would settle this in either direction. Until then the correct posture is that TabFM's contamination story is structurally strong and evidentially unaudited — and that the wrapper's dozen tuned defaults (softmax_temperature=0.9, outlier_threshold=4.0, nnls_beta=0.75, n_estimators=32) were fit to something nobody has named.

  6. Google's own TPU results and TabArena's verified GPU re-run differ by a median of 0.23%, and by up to 35% relative on the lowest-error datasets.

    Merged the four parquet files in google-research/tabfm/results on (dataset, fold), averaged per dataset, and compared against the e field in the leaderboard's per-dataset payload for TabFM (default). 41 of 51 agree within 1%. I am attributing the gap to accelerator and backend — the parquet filenames say jax-tpu, the board says GPU — and to bfloat16 compute over a 32-member ensemble. If the board's TabFM row turns out to be Google's own submitted numbers rather than a re-run, the comparison is between two things I have mislabelled and the inference is void.

  7. Elo on TabArena is a property of the pool, not the model.

    Six ratings for one checkpoint, 1727 to 1989, captured 2026-09-19 from website_leaderboard.csv across entrant sets and task slices. If the ratings were stable across pools this would be a non-point. Re-pull the same files on a later date and the numbers will have moved again without a byte of the checkpoint changing — which is the claim, not a caveat about it.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "The tabular foundation model where 98.8% of the weights never see a cell", ai.thesatyajit.com, September 2026.

bibtex
@misc{ghana2026tabfm,
  author = {Satyajit Ghana},
  title  = {The tabular foundation model where 98.8% of the weights never see a cell},
  url    = {https://ai.thesatyajit.com/articles/tabfm},
  year   = {2026}
}
share