LightPFN 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lightpfn/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """LightPFN: compact tabular inference, pretrained only on synthetic data."""
2
+
3
+ __version__ = "1.0.0"
4
+ __all__ = ["Config", "LightPFN", "LightPFNClassifier", "load_model", "load_pretrained", "save_model", "__version__"]
5
+
6
+
7
+ def __getattr__(name):
8
+ if name in ("Config", "LightPFN"):
9
+ from lightpfn.model import lightpfn
10
+ value = getattr(lightpfn, name)
11
+ elif name == "LightPFNClassifier":
12
+ try:
13
+ from lightpfn.sklearn import LightPFNClassifier
14
+ except ModuleNotFoundError as exc:
15
+ if exc.name != "sklearn":
16
+ raise
17
+ raise ImportError("LightPFNClassifier requires scikit-learn: `pip install scikit-learn`.") from exc
18
+ value = LightPFNClassifier
19
+ elif name in ("load_model", "load_pretrained", "save_model"):
20
+ from lightpfn import checkpoint
21
+ value = getattr(checkpoint, name)
22
+ else:
23
+ raise AttributeError(name)
24
+ globals()[name] = value
25
+ return value
lightpfn/checkpoint.py ADDED
@@ -0,0 +1,107 @@
1
+ """Safe local checkpoints and revision-pinned Hugging Face downloads.
2
+
3
+ The release format is model.safetensors plus config.json. Legacy tensor/dict
4
+ checkpoints remain supported using PyTorch's restricted weights-only loader.
5
+ There is deliberately no fallback to unrestricted pickle.
6
+ """
7
+
8
+ import json
9
+ from collections.abc import Mapping
10
+ from dataclasses import asdict
11
+ from importlib.resources import files
12
+ from pathlib import Path
13
+
14
+ import torch
15
+
16
+ from lightpfn.model.lightpfn import Config, LightPFN
17
+
18
+
19
+ def _safetensors():
20
+ try:
21
+ from safetensors.torch import load_file, save_file
22
+ except ImportError as exc:
23
+ raise ImportError("Safetensors weights require `pip install safetensors`.") from exc
24
+ return load_file, save_file
25
+
26
+
27
+ def _config(values):
28
+ if not isinstance(values, Mapping):
29
+ raise ValueError("Checkpoint config must be a mapping of architecture parameters.")
30
+ values = dict(values)
31
+ if "group_offsets" in values:
32
+ values["group_offsets"] = tuple(values["group_offsets"])
33
+ return Config(**values)
34
+
35
+
36
+ def load_model(path, device="cpu"):
37
+ """Load a safetensors directory/file or a weights-only compatible .pt file.
38
+
39
+ Safetensors files need config.json in the same directory. Full training
40
+ checkpoints containing arbitrary Python objects are intentionally rejected.
41
+ """
42
+ path = Path(path)
43
+ if path.is_dir():
44
+ path = path / "model.safetensors"
45
+ if path.suffix == ".safetensors":
46
+ load_file, _ = _safetensors()
47
+ config = json.loads(path.with_name("config.json").read_text(encoding="utf-8"))
48
+ state = load_file(str(path), device="cpu")
49
+ else:
50
+ checkpoint = torch.load(path, map_location="cpu", weights_only=True)
51
+ if not isinstance(checkpoint, Mapping) or "config" not in checkpoint:
52
+ raise ValueError("Checkpoint must contain 'config' and 'ema' or 'model' weights.")
53
+ config = checkpoint["config"]
54
+ state = checkpoint.get("ema", checkpoint.get("model"))
55
+ if not isinstance(state, Mapping) or not state or not all(
56
+ isinstance(key, str) and isinstance(value, torch.Tensor) for key, value in state.items()
57
+ ):
58
+ raise ValueError("Checkpoint weights must be a nonempty tensor state dictionary.")
59
+ model = LightPFN(_config(config))
60
+ model.load_state_dict(state, strict=True)
61
+ return model.to(device).eval()
62
+
63
+
64
+ def save_model(model, directory):
65
+ """Export unfused inference weights and JSON config, excluding optimizer state."""
66
+ if getattr(model, "is_folded", False):
67
+ raise ValueError("Export the original model; folded weights use a different layout.")
68
+ _, save_file = _safetensors()
69
+ directory = Path(directory)
70
+ directory.mkdir(parents=True, exist_ok=True)
71
+ state = {key: value.detach().cpu().contiguous().clone() for key, value in model.state_dict().items()}
72
+ save_file(state, str(directory / "model.safetensors"), metadata={"format": "pt"})
73
+ (directory / "config.json").write_text(json.dumps(asdict(model.cfg), indent=2) + "\n", encoding="utf-8")
74
+ return directory
75
+
76
+
77
+ def pretrained_spec():
78
+ """Return the packaged model repository and immutable revision."""
79
+ return json.loads(files("lightpfn").joinpath("pretrained.json").read_text(encoding="utf-8"))
80
+
81
+
82
+ def load_pretrained(*, repo_id=None, revision=None, device="cpu", cache_dir=None, local_files_only=False):
83
+ """Load the released model, or an explicit repo and revision using cached HF authentication.
84
+
85
+ A custom repository requires a revision. No remote Python code is executed.
86
+ For private repositories authenticate with `hf auth login` or HF_TOKEN.
87
+ """
88
+ try:
89
+ from huggingface_hub import hf_hub_download
90
+ except ImportError as exc:
91
+ raise ImportError("Hugging Face downloads require `pip install huggingface-hub`.") from exc
92
+ spec = pretrained_spec()
93
+ repo_id = spec["repo_id"] if repo_id is None else repo_id
94
+ if revision is None:
95
+ if repo_id != spec["repo_id"]:
96
+ raise ValueError("Provide revision when using a custom Hugging Face repository.")
97
+ revision = spec["revision"]
98
+ if not revision:
99
+ raise ValueError("No pretrained revision configured; provide a checkpoint or explicit revision.")
100
+ options = dict(repo_id=repo_id, revision=revision, cache_dir=cache_dir, local_files_only=local_files_only)
101
+ config_path = Path(hf_hub_download(filename="config.json", **options))
102
+ # Use the resolved commit from the snapshot path for both files, even when a
103
+ # caller explicitly chose a mutable branch or tag.
104
+ resolved = config_path.parent.name
105
+ options["revision"] = resolved
106
+ weights_path = Path(hf_hub_download(filename="model.safetensors", **options))
107
+ return load_model(weights_path, device=device)
lightpfn/device.py ADDED
@@ -0,0 +1,51 @@
1
+ """Inference device: "auto" takes torch's GPU backend when there is one (CUDA on NVIDIA, also ROCm builds of
2
+ torch, which use the same "cuda" device), otherwise a GPU with a Vulkan driver (AMD, Intel or NVIDIA,
3
+ through wgpu), otherwise the CPU. Each can be asked for explicitly: "cuda[:i]", "vulkan[:i]" (index into
4
+ lightpfn.vulkan.adapters()), "cpu". The environment variable LIGHTPFN_DEVICE replaces "auto" with any of
5
+ these, e.g. LIGHTPFN_DEVICE=cpu to keep a GPU free.
6
+ """
7
+
8
+ import os
9
+
10
+ import torch
11
+
12
+ DEVICE_ENV = "LIGHTPFN_DEVICE"
13
+
14
+
15
+ def resolve_device(device="auto"):
16
+ """The device string LightPFNClassifier runs on: "cuda[:i]", "vulkan[:i]", "cpu" (or "mps")."""
17
+ device = "auto" if device is None else str(device).strip().lower()
18
+ if device == "auto":
19
+ device = os.environ.get(DEVICE_ENV, "").strip().lower() or "auto"
20
+ if device == "auto":
21
+ if torch.cuda.is_available():
22
+ return "cuda"
23
+ from lightpfn import vulkan
24
+
25
+ return "vulkan" if vulkan.is_available() else "cpu"
26
+ kind, _, index = device.partition(":")
27
+ if ":" in device and not index:
28
+ raise ValueError(f"device {device!r}: missing index after ':'")
29
+ if index and not index.isdigit():
30
+ raise ValueError(f"device {device!r}: the index after ':' must be a number")
31
+ if kind == "cpu":
32
+ return "cpu"
33
+ if kind == "cuda":
34
+ if not torch.cuda.is_available():
35
+ raise RuntimeError("device 'cuda' requested, but this torch build sees no CUDA/ROCm GPU")
36
+ return device
37
+ if kind == "vulkan":
38
+ from lightpfn import vulkan
39
+
40
+ if not vulkan.is_available(int(index) if index else None):
41
+ raise RuntimeError(f"device {device!r} requested, but no Vulkan adapter was found: it needs a GPU "
42
+ "driver with Vulkan and `pip install wgpu` (adapters: lightpfn.vulkan.adapters())")
43
+ return device
44
+ if kind == "mps":
45
+ return device
46
+ raise ValueError(f"unknown device {device!r}: use 'auto', 'cpu', 'cuda[:i]' or 'vulkan[:i]'")
47
+
48
+
49
+ def kind(device):
50
+ """"cpu", "cuda", "vulkan" or "mps" of a resolved device string."""
51
+ return str(device).partition(":")[0]
@@ -0,0 +1 @@
1
+ """LightPFN architecture; the inference package does not import synthetic priors."""
@@ -0,0 +1,117 @@
1
+ """Category statistics of the context rows for the categorical adapter (Config.cat_adapter).
2
+
3
+ Every categorical cell receives the class distribution of its category among the context rows,
4
+ as a deviation from the class prior, plus count features. Query rows use every context row of
5
+ their category. A context row uses only the context rows that come before it in a random order
6
+ of the rows (CatBoost's ordered target statistics), so its own label never enters its features,
7
+ and no label is ever correlated with its own statistics beyond the true dependence (leave-one-out
8
+ statistics are: the model collapsed on them in our tests). The count of rows the
9
+ statistics rest on is a feature, so the model can weigh a context row's noisier estimate.
10
+
11
+ Missing values form one category of their own. An unseen category gets zero features.
12
+ Everything is vectorized over tasks and columns; only the categorical columns are processed.
13
+ """
14
+
15
+ import math
16
+
17
+ import torch
18
+ import torch.nn.functional as F
19
+
20
+ N_COUNT = 3 # log1p(rows the statistics use), log1p(other rows of the category), their fraction of the rows
21
+
22
+
23
+ def _keys(x):
24
+ """Category keys: the values, with missing values as one key that sorts last."""
25
+ x = x.float()
26
+ return torch.where(torch.isnan(x), torch.full_like(x, math.inf), x)
27
+
28
+
29
+ def _features(counts, n_used, n_freq, prior, n, smooth):
30
+ """counts (..., C) class counts of the rows used, n_used/n_freq (...) row counts, prior (B, C)
31
+ broadcastable to counts; returns (..., C + N_COUNT)."""
32
+ delta = (counts - n_used[..., None] * prior) / (n_used[..., None] + smooth)
33
+ cnt = torch.stack([torch.log1p(n_used) / 4, torch.log1p(n_freq) / 4, n_freq / n], -1)
34
+ return torch.cat([delta, cnt], -1)
35
+
36
+
37
+ def context_stats(x, y, cat, n_classes, smooth=1.0, perm=None, generator=None):
38
+ """x: (B, n, m) context values (NaN = missing), y: (B, n) classes in 0..n_classes-1, cat: (B, m) bool.
39
+ perm: (B, n) the row order of the ordered statistics. Default: random per task from the global RNG, or,
40
+ with a generator, one order drawn from it for every task (the same whatever the batch).
41
+
42
+ Returns (feats, table): feats (B, n, m, n_classes + N_COUNT) float32, zero on numerical columns, and
43
+ the table query_stats needs; (None, None) when no column is categorical."""
44
+ B, n, m = x.shape
45
+ C = int(n_classes)
46
+ dev = x.device
47
+ mc = int(cat.sum(1).max()) if cat.numel() else 0
48
+ if mc == 0:
49
+ return None, None
50
+ # the categorical columns of each task first, in order; the tail of a task with fewer is padding
51
+ cols = torch.argsort((~cat).to(torch.int8), dim=1, stable=True)[:, :mc] # (B, mc)
52
+ valid = torch.gather(cat, 1, cols)
53
+ keys = _keys(torch.gather(x, 2, cols[:, None].expand(B, n, mc)))
54
+ if perm is None:
55
+ if generator is None:
56
+ perm = torch.argsort(torch.rand(B, n, device=dev), 1)
57
+ else:
58
+ perm = torch.argsort(torch.rand(n, generator=generator)).expand(B, n)
59
+ perm = perm.to(dev)
60
+ pe = perm[..., None].expand(B, n, mc)
61
+ kp = torch.gather(keys, 1, pe)
62
+ vs, order = torch.sort(kp, dim=1, stable=True) # stable: rows of a category keep the random order
63
+ rows = torch.gather(pe, 1, order) # original row of every sorted position
64
+ ys = torch.gather(y.long()[..., None].expand(B, n, mc), 1, rows)
65
+ oh = F.one_hot(ys, C).float() # (B, n, mc, C)
66
+ start = torch.ones_like(vs, dtype=torch.bool)
67
+ start[:, 1:] = vs[:, 1:] != vs[:, :-1]
68
+ end = torch.ones_like(start)
69
+ end[:, :-1] = start[:, 1:]
70
+ ar = torch.arange(n, device=dev)[None, :, None]
71
+ first = torch.where(start, ar, 0).cummax(1).values # first sorted position of the category
72
+ last = (n - 1 - torch.where(end.flip(1), ar, 0).cummax(1).values).flip(1) # last one
73
+ incl = oh.cumsum(1)
74
+ excl = incl - oh
75
+ del oh
76
+ base = torch.gather(excl, 1, first[..., None].expand_as(excl))
77
+ prefix = excl.sub_(base) # rows of the category before this one in the random order
78
+ # Query counts are constant within a category. Cache one entry per category rather than n
79
+ # repeated entries per column (hundreds of MB per estimator at 60k rows / 100 columns).
80
+ n_keys = int((start.sum(1) * valid).max())
81
+ category = start.long().cumsum(1) - 1
82
+ bi, ri, ci = torch.where(end & valid[:, None])
83
+ ki = category[bi, ri, ci]
84
+ table_keys = torch.full((B, mc, n_keys), math.inf, device=dev)
85
+ table_total = torch.zeros(B, mc, n_keys, C, device=dev)
86
+ table_keys[bi, ci, ki] = vs[bi, ri, ci]
87
+ table_total[bi, ci, ki] = incl[bi, ri, ci] - base[bi, ri, ci]
88
+ del incl, excl, base
89
+ prior = F.one_hot(y.long(), C).float().mean(1)[:, None, None] # (B, 1, 1, C)
90
+ n_used = (ar - first).float()
91
+ feats = _features(prefix, n_used, (last - first).float(), prior, n, smooth) * valid[:, None, :, None]
92
+ out = torch.zeros(B, n, mc, feats.shape[-1], device=dev)
93
+ out.scatter_(1, rows[..., None].expand_as(feats), feats)
94
+ full = torch.zeros(B, n, m, feats.shape[-1], device=dev)
95
+ full.scatter_(2, cols[:, None, :, None].expand_as(out), out) # cols is a permutation prefix: no repeats
96
+ table = dict(cols=cols, valid=valid, keys=table_keys,
97
+ total=table_total, prior=prior, n=n, smooth=smooth)
98
+ return full, table
99
+
100
+
101
+ def query_stats(table, x):
102
+ """Features (B, M, m, C + N_COUNT) of query rows x: (B, M, m) from the table of context_stats."""
103
+ if table is None:
104
+ return None
105
+ B, M, m = x.shape
106
+ cols, valid, keys, total = table["cols"], table["valid"], table["keys"], table["total"]
107
+ mc, n_keys, C = keys.shape[1], keys.shape[2], total.shape[-1]
108
+ q = _keys(torch.gather(x, 2, cols[:, None].expand(B, M, mc))).transpose(1, 2).contiguous() # (B, mc, M)
109
+ pos = torch.searchsorted(keys, q).clamp(max=n_keys - 1)
110
+ hit = torch.gather(keys, 2, pos) == q
111
+ counts = torch.gather(total, 2, pos[..., None].expand(B, mc, M, C)) * hit[..., None]
112
+ n_used = counts.sum(-1)
113
+ feats = _features(counts, n_used, n_used, table["prior"], table["n"], table["smooth"])
114
+ feats = (feats * valid[:, :, None, None]).transpose(1, 2) # (B, M, mc, F)
115
+ full = torch.zeros(B, M, m, feats.shape[-1], device=x.device)
116
+ full.scatter_(2, cols[:, None, :, None].expand_as(feats), feats)
117
+ return full
@@ -0,0 +1,190 @@
1
+ """Building blocks shared by the three stages of the model.
2
+
3
+ Tensors are (batch, sequence, dim) unless a name says otherwise. Attention runs through
4
+ F.scaled_dot_product_attention, so the same code uses flash kernels on GPU and fused CPU kernels.
5
+ """
6
+
7
+ import math
8
+
9
+ import torch
10
+ import torch.nn.functional as F
11
+ from torch import nn
12
+
13
+
14
+ class MLP(nn.Module):
15
+ """Two-layer GELU feed-forward block; the output projection starts at zero so every residual
16
+ block starts as the identity (as in TabPFN-3)."""
17
+
18
+ def __init__(self, dim, hidden):
19
+ super().__init__()
20
+ self.fc1 = nn.Linear(dim, hidden, bias=False)
21
+ self.fc2 = nn.Linear(hidden, dim, bias=False)
22
+ nn.init.zeros_(self.fc2.weight)
23
+
24
+ def forward(self, x):
25
+ return self.fc2(F.gelu(self.fc1(x)))
26
+
27
+
28
+ class SoftmaxScaling(nn.Module):
29
+ """Scales attention queries as a function of the number of keys n (TabPFN-3 SoftmaxScalingMLP):
30
+ q * base(log n) * (1 + tanh(mod(q))). Keeps attention sharp when the context grows far beyond
31
+ the lengths seen in training. base starts at 1 so the layer starts as a no-op."""
32
+
33
+ def __init__(self, n_heads, head_dim, hidden=64):
34
+ super().__init__()
35
+ self.n_heads, self.head_dim = n_heads, head_dim
36
+ self.base = nn.Sequential(nn.Linear(1, hidden), nn.GELU(), nn.Linear(hidden, n_heads * head_dim))
37
+ self.mod = nn.Sequential(nn.Linear(head_dim, hidden), nn.GELU(), nn.Linear(hidden, head_dim))
38
+ nn.init.zeros_(self.base[2].weight)
39
+ nn.init.ones_(self.base[2].bias)
40
+ nn.init.zeros_(self.mod[2].weight)
41
+ nn.init.zeros_(self.mod[2].bias)
42
+
43
+ def forward(self, q, n):
44
+ """q: (B, H, L, D) queries, n: number of keys."""
45
+ logn = torch.full((1, 1), math.log(max(n, 2)), device=q.device, dtype=q.dtype)
46
+ base = self.base(logn).view(1, self.n_heads, 1, self.head_dim)
47
+ return q * base * (1 + torch.tanh(self.mod(q)))
48
+
49
+
50
+ # CUDA attention kernels put the batch on a grid dimension of at most 65535 blocks: a larger batch fails
51
+ # with "invalid argument" (the row stages of a 2-task group of 50k-row tables have 100k rows in the batch).
52
+ SDPA_MAX_BATCH = 32768
53
+
54
+
55
+ def sdpa(q, k, v, mask=None):
56
+ """F.scaled_dot_product_attention in chunks of at most SDPA_MAX_BATCH along the batch dim (a size-1
57
+ batch dim of k, v or mask broadcasts)."""
58
+ B = q.shape[0]
59
+ if B <= SDPA_MAX_BATCH:
60
+ return F.scaled_dot_product_attention(q, k, v, attn_mask=mask)
61
+ out = []
62
+ for i in range(0, B, SDPA_MAX_BATCH):
63
+ part = [t if t is None or t.shape[0] == 1 else t[i:i + SDPA_MAX_BATCH] for t in (k, v, mask)]
64
+ out.append(F.scaled_dot_product_attention(q[i:i + SDPA_MAX_BATCH], *part[:2], attn_mask=part[2]))
65
+ return torch.cat(out)
66
+
67
+
68
+ def rope(x, pos, base=10000.0, interleaved=False):
69
+ """Rotary position embedding (rotate-half form) on the last dim of x: (..., L, D) with
70
+ positions pos: (L,). interleaved=True rotates the pairs (2i, 2i + 1) instead of (i, i + D/2),
71
+ with one complex multiply: the same function for weights passed through interleave_rotary()."""
72
+ d = x.shape[-1]
73
+ inv_freq = base ** (-torch.arange(0, d, 2, device=x.device, dtype=torch.float32) / d)
74
+ ang = pos.to(torch.float32)[:, None] * inv_freq[None] # (L, D/2)
75
+ if interleaved and x.dtype in (torch.float32, torch.float64):
76
+ rot = torch.complex(ang.cos(), ang.sin()).to(torch.complex64 if x.dtype == torch.float32 else torch.complex128)
77
+ return torch.view_as_real(torch.view_as_complex(x.unflatten(-1, (d // 2, 2))) * rot).flatten(-2)
78
+ cos, sin = ang.cos().to(x.dtype), ang.sin().to(x.dtype)
79
+ if interleaved: # reduced precision (autocast): the same arithmetic as below on adjacent pairs
80
+ x1, x2 = x[..., 0::2], x[..., 1::2]
81
+ return torch.stack([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1).flatten(-2)
82
+ x1, x2 = x[..., : d // 2], x[..., d // 2 :]
83
+ return torch.cat([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1)
84
+
85
+
86
+ class Attention(nn.Module):
87
+ """Multi-head attention with separate query and key/value inputs.
88
+
89
+ kv_heads_query < n_heads lets a set of queries use only the first key/value heads (multi-query
90
+ attention for test rows, as in TabPFN-3.5): keys/values of the other heads never need to be
91
+ cached for prediction."""
92
+
93
+ def __init__(self, dim, n_heads, scaling=False):
94
+ super().__init__()
95
+ assert dim % n_heads == 0
96
+ self.n_heads, self.head_dim = n_heads, dim // n_heads
97
+ self.q = nn.Linear(dim, dim, bias=False)
98
+ self.kv = nn.Linear(dim, 2 * dim, bias=False)
99
+ self.out = nn.Linear(dim, dim, bias=False)
100
+ nn.init.zeros_(self.out.weight)
101
+ self.scaling = SoftmaxScaling(n_heads, self.head_dim) if scaling else None
102
+
103
+ def split(self, x):
104
+ B, L, _ = x.shape
105
+ return x.view(B, L, self.n_heads, self.head_dim).transpose(1, 2) # (B, H, L, D)
106
+
107
+ def keys_values(self, x):
108
+ k, v = self.kv(x).chunk(2, dim=-1)
109
+ return self.split(k), self.split(v)
110
+
111
+ def attend(self, x, k, v, mask=None, q_rope=None, kv_heads=None):
112
+ """x: (B, Lq, dim) queries; k, v: (B, Hkv, Lk, D). kv_heads=h: all query heads use the
113
+ first h key/value heads."""
114
+ q = self.split(self.q(x))
115
+ if q_rope is not None:
116
+ q = q_rope(q)
117
+ if self.scaling is not None:
118
+ q = self.scaling(q, k.shape[2])
119
+ if kv_heads is not None:
120
+ k = k[:, :kv_heads].repeat_interleave(self.n_heads // kv_heads, dim=1)
121
+ v = v[:, :kv_heads].repeat_interleave(self.n_heads // kv_heads, dim=1)
122
+ o = sdpa(q, k, v, mask)
123
+ B, H, L, D = o.shape
124
+ return self.out(o.transpose(1, 2).reshape(B, L, H * D))
125
+
126
+
127
+ class Block(nn.Module):
128
+ """Pre-norm residual block: attention of x on a key/value sequence, then an MLP."""
129
+
130
+ def __init__(self, dim, n_heads, ff_factor=2, scaling=False, ff_hidden=None):
131
+ super().__init__()
132
+ self.norm_q = nn.RMSNorm(dim)
133
+ self.norm_kv = nn.RMSNorm(dim)
134
+ self.norm_ff = nn.RMSNorm(dim)
135
+ self.attn = Attention(dim, n_heads, scaling)
136
+ self.mlp = MLP(dim, dim * ff_factor if ff_hidden is None else ff_hidden)
137
+
138
+ def keys_values(self, kv_input):
139
+ return self.attn.keys_values(self.norm_kv(kv_input))
140
+
141
+ def forward(self, x, k, v, mask=None, q_rope=None, kv_heads=None):
142
+ x = x + self.attn.attend(self.norm_q(x), k, v, mask, q_rope, kv_heads)
143
+ return x + self.mlp(self.norm_ff(x))
144
+
145
+
146
+ class FoldedRMSNorm(nn.Module):
147
+ """RMSNorm whose weight was folded into the linear layer that reads it (inference only): one
148
+ reduction and one multiply instead of the unfused CPU kernels. Same eps as nn.RMSNorm (eps=None:
149
+ that of the accumulation dtype, float32 for reduced precision), reduction in at least float32."""
150
+
151
+ def __init__(self, dim, eps=None):
152
+ super().__init__()
153
+ self.dim, self.eps = dim, eps
154
+
155
+ def forward(self, x):
156
+ acc = torch.promote_types(x.dtype, torch.float32)
157
+ eps = torch.finfo(acc).eps if self.eps is None else self.eps
158
+ ms = torch.linalg.vector_norm(x, dim=-1, keepdim=True, dtype=acc).square_().div_(self.dim)
159
+ return x * ms.add_(eps).rsqrt_().to(x.dtype)
160
+
161
+
162
+ @torch.no_grad()
163
+ def fold_norm(norm, *linears):
164
+ """Moves the weight of an RMSNorm into the linear layers fed by it; returns the weightless norm."""
165
+ for lin in linears:
166
+ lin.weight.mul_(norm.weight)
167
+ return FoldedRMSNorm(norm.weight.numel(), norm.eps)
168
+
169
+
170
+ @torch.no_grad()
171
+ def interleave_rotary(block):
172
+ """Reorders the query and key dims of each head of a block from (i, i + D/2) pairs to adjacent
173
+ (2i, 2i + 1) pairs: q.k is unchanged (same permutation on both), and rope(..., interleaved=True)
174
+ rotates the same pairs."""
175
+ a = block.attn
176
+ H, D = a.n_heads, a.head_dim
177
+ pairs = torch.stack([torch.arange(D // 2), torch.arange(D // 2) + D // 2], -1).flatten()
178
+ rows = (torch.arange(H)[:, None] * D + pairs).flatten().to(a.q.weight.device)
179
+ a.q.weight.copy_(a.q.weight[rows])
180
+ a.kv.weight[: H * D].copy_(a.kv.weight[rows])
181
+
182
+
183
+ class OrthogonalEmbedding(nn.Embedding):
184
+ """Label embedding with orthonormal initial rows (TabPFN-3 TrainableOrthogonalEmbedding)."""
185
+
186
+ def __init__(self, n, dim):
187
+ super().__init__(n, dim)
188
+ with torch.no_grad():
189
+ q, _ = torch.linalg.qr(torch.randn(dim, min(n, dim)))
190
+ self.weight[: q.shape[1]] = q.T