| """Constructed ReLU circuit for exact (a * b) mod p. |
| |
| This module separates two concerns on purpose: |
| |
| * **Topology** — which ReLU/linear/conv primitives run, in what order, and at |
| what tier geometry (limb count ``n``, derived from the prime's bit width). |
| Built by :func:`build_topology`; it is the same object the frontier-track |
| training experiment will later optimise from random init. |
| * **Weight filling** — the numeric content of every primitive (step |
| thresholds, gating constants, shift amounts). Supplied by an *initializer*. |
| :class:`ConstructedInit` fills the bit-exact arithmetic-circuit weights; |
| :class:`RandomInit` fills the same slots from a seeded RNG so the identical |
| topology is a trainable network. The constructed weights are therefore one |
| initializer among several, not a baked-in forward pass. |
| |
| Forward-pass discipline (the competition's letter): every operation is a |
| linear map, a ReLU, or a 1-D convolution. There is no integer tensor |
| arithmetic, no ``einsum`` on the inputs, and no product of two activations. |
| Multiplication is done by the binary-gated-product identity, which replaces |
| every bilinear op with a ReLU. Weights are all in ``{0, +-1, +-2^t}`` and are |
| exactly representable in float32 storage; compute runs in float64. |
| |
| The three identities (each exact on integer-valued floats): |
| |
| 1. step gate: ``step_tau(x) = relu(x - tau + 1) - relu(x - tau)`` is ``1{x >= tau}``. |
| 2. gated product: for ``0 <= v < 2^16`` and bit ``b``, ``b*v = relu(v - 2^16*(1-b))``. |
| 3. boolean: ``AND = relu(a+b-1)``, ``XOR = a+b-2*AND``, ``OR = a+b-AND``. |
| |
| See ``src/constructed/README.md`` for the topology/initializer split and the |
| companion feasibility spec for the per-tier envelope. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import math |
| from dataclasses import dataclass |
|
|
| import torch |
| import torch.nn as nn |
|
|
| LIMB_BITS = 16 |
| B = 1 << LIMB_BITS |
|
|
|
|
| def n_limbs_for_bits(max_bits: int) -> int: |
| """Limb count for a prime up to ``max_bits`` wide, base 2^16.""" |
| return max(1, math.ceil(max_bits / LIMB_BITS)) |
|
|
|
|
| |
| |
| |
| |
| |
|
|
| @dataclass(frozen=True) |
| class Consts: |
| """The learnable constants threaded through the primitives. |
| |
| ``one`` is the step-gate unit width (constructed: ``1.0``); ``base`` is the |
| gated-product gating constant (constructed: ``2^16``). Passing them as data |
| rather than hardcoding makes the topology genuinely trainable and makes the |
| weight-perturbation collapse test bite: a wrong ``one`` or ``base`` breaks |
| every comparator and gated product in the circuit. |
| """ |
|
|
| one: float = 1.0 |
| base: float = float(B) |
|
|
|
|
| _CONSTRUCTED = Consts() |
|
|
|
|
| class _GateCounter: |
| """Opt-in tally of ReLU gates and circuit depth, for the per-tier audit. |
| |
| Counting is a measurement aid only; it never changes the forward result. |
| A "gate" is one ReLU lane (per scalar position), accumulated across the |
| sequential pipeline so the number is comparable to the doc's ReLUs/prob. |
| """ |
|
|
| def __init__(self): |
| self.active = False |
| self.gates = 0 |
| self.relu_calls = 0 |
|
|
| def add(self, t: torch.Tensor) -> None: |
| if not self.active: |
| return |
| self.relu_calls += 1 |
| |
| self.gates += t.shape[-1] if t.dim() else 1 |
|
|
|
|
| _COUNTER = _GateCounter() |
|
|
|
|
| def relu(t: torch.Tensor) -> torch.Tensor: |
| _COUNTER.add(t) |
| return torch.clamp(t, min=0.0) |
|
|
|
|
| class count_gates: |
| """Context manager: tally ReLU gates/calls during the enclosed forward. |
| |
| >>> with count_gates() as c: |
| ... circuit(x_limbs, y_bits, p_bits, mu_bits) |
| >>> c.gates, c.relu_calls |
| """ |
|
|
| def __enter__(self) -> _GateCounter: |
| _COUNTER.active = True |
| _COUNTER.gates = 0 |
| _COUNTER.relu_calls = 0 |
| return _COUNTER |
|
|
| def __exit__(self, *exc) -> None: |
| _COUNTER.active = False |
|
|
|
|
| def step_gate(x: torch.Tensor, tau: float, one: float = 1.0) -> torch.Tensor: |
| """Exact ``1{x >= tau}`` on integer-valued ``x`` via two ReLUs (identity 1). |
| |
| ``one`` is the unit-step width: the constructed value is ``1.0``. It is |
| surfaced as a learnable constant so the topology is trainable and so a |
| perturbed value provably breaks the comparator (the operational test). |
| """ |
| return relu(x - tau + one) - relu(x - tau) |
|
|
|
|
| def gated_product(v: torch.Tensor, b: torch.Tensor, base: float = float(B)) -> torch.Tensor: |
| """Exact ``b*v`` for bit ``b`` and ``0 <= v < 2^16`` via one ReLU (identity 2). |
| |
| ``base`` is the gating constant ``2^16``; surfaced as a learnable constant |
| for the same reason as ``one`` above. |
| """ |
| return relu(v - base * (1.0 - b)) |
|
|
|
|
| def bit_and(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: |
| return relu(a + b - 1.0) |
|
|
|
|
| def bit_xor(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: |
| return a + b - 2.0 * bit_and(a, b) |
|
|
|
|
| def peel_bits( |
| value: torch.Tensor, width: int, consts: Consts = _CONSTRUCTED |
| ) -> torch.Tensor: |
| """MSB-first sequential bit peel of an integer in ``[0, 2^width)``. |
| |
| Returns a ``(..., width)`` tensor of bits, LSB first. Each bit is a |
| 2-ReLU step comparator at a power-of-2 threshold; the residual update |
| ``r <- r - b*2^i`` is linear because ``b`` is already exactly ``0/1``. |
| """ |
| r = value.clone() |
| bits = [] |
| for i in range(width - 1, -1, -1): |
| b = step_gate(r, float(1 << i), consts.one) |
| r = r - b * float(1 << i) |
| bits.append(b) |
| bits.reverse() |
| return torch.stack(bits, dim=-1) |
|
|
|
|
| def limbs_from_columns( |
| cols: torch.Tensor, col_width: int, consts: Consts = _CONSTRUCTED |
| ) -> torch.Tensor: |
| """Carry-propagate a base-2^16 carry-save column vector into clean limbs. |
| |
| ``cols`` is ``(..., m)`` of nonnegative integer-valued sums, each |
| ``< 2^col_width``. Returns ``(..., m + extra)`` base-2^16 limbs of the same |
| total value. The ripple uses :func:`peel_bits` per column (the doc's bit |
| peel + re-bin), splitting each running sum into a low 16-bit limb and the |
| carry into the next column. Pure linear + ReLU. |
| """ |
| m = cols.shape[-1] |
| lead = cols.shape[:-1] |
| carry = torch.zeros(lead, dtype=cols.dtype) |
| out = [] |
|
|
| def split(s: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: |
| bits = peel_bits(s, col_width, consts) |
| weights_lo = torch.tensor( |
| [float(1 << j) for j in range(LIMB_BITS)], dtype=cols.dtype |
| ) |
| weights_hi = torch.tensor( |
| [float(1 << (j - LIMB_BITS)) for j in range(LIMB_BITS, col_width)], |
| dtype=cols.dtype, |
| ) |
| lo = (bits[..., :LIMB_BITS] * weights_lo).sum(dim=-1) |
| hi = (bits[..., LIMB_BITS:] * weights_hi).sum(dim=-1) |
| return lo, hi |
|
|
| for i in range(m): |
| s = cols[..., i] + carry |
| lo, carry = split(s) |
| out.append(lo) |
| |
| while bool((carry > 0).any()): |
| lo, carry = split(carry) |
| out.append(lo) |
| return torch.stack(out, dim=-1) |
|
|
|
|
| |
| |
| |
| |
| |
| |
|
|
| def schoolbook_mul( |
| x_limbs: torch.Tensor, |
| y_bits: torch.Tensor, |
| peak: list | None = None, |
| consts: Consts = _CONSTRUCTED, |
| ) -> torch.Tensor: |
| """``x * y`` where ``x`` is base-2^16 limbs and ``y`` is bits (LSB first). |
| |
| Gated partial products (identity 2) accumulated into carry-save columns, |
| then carry-propagated. Each column sum stays ``< n * 2^32`` (the doc's |
| bound), inside float64's exact range with a wide margin. If ``peak`` is a |
| list, the maximum carry-save column magnitude seen is appended to it (the |
| largest intermediate of the whole circuit, for the precision audit). |
| """ |
| n = x_limbs.shape[-1] |
| nbits = y_bits.shape[-1] |
| lead = x_limbs.shape[:-1] |
| max_col = n + (nbits + LIMB_BITS - 1) // LIMB_BITS + 2 |
| col_width = (2 * LIMB_BITS) + max(1, (n).bit_length() + nbits.bit_length()) |
| cols = [torch.zeros(lead, dtype=x_limbs.dtype) for _ in range(max_col)] |
| for j in range(nbits): |
| bj = y_bits[..., j] |
| off = j % LIMB_BITS |
| base_col = j // LIMB_BITS |
| scale = float(1 << off) |
| for i in range(n): |
| gx = gated_product(x_limbs[..., i], bj, consts.base) |
| cols[base_col + i] = cols[base_col + i] + gx * scale |
| stacked = torch.stack(cols, dim=-1) |
| if peak is not None and stacked.numel(): |
| peak.append(float(stacked.abs().max())) |
| return limbs_from_columns(stacked, col_width, consts) |
|
|
|
|
| def int_to_limbs(value: int, n: int) -> list[int]: |
| return [(value >> (LIMB_BITS * i)) & (B - 1) for i in range(n)] |
|
|
|
|
| def int_to_bits(value: int, nbits: int) -> list[int]: |
| return [(value >> i) & 1 for i in range(nbits)] |
|
|
|
|
| def limbs_to_int(limbs: torch.Tensor) -> int: |
| vals = [int(round(float(v))) for v in limbs.flatten().tolist()] |
| return sum(v << (LIMB_BITS * i) for i, v in enumerate(vals)) |
|
|
|
|
| def bits_to_int(bits: torch.Tensor) -> int: |
| vals = [int(round(float(v))) for v in bits.flatten().tolist()] |
| return sum(v << i for i, v in enumerate(vals)) |
|
|
|
|
| |
| |
| |
|
|
| @dataclass(frozen=True) |
| class TierGeometry: |
| """Geometry the circuit is built for; derived from the prime bit width.""" |
|
|
| tier_idx: int |
| max_bits: int |
|
|
| @property |
| def n(self) -> int: |
| return n_limbs_for_bits(self.max_bits) |
|
|
| @property |
| def k(self) -> int: |
| |
| return self.n |
|
|
|
|
| @dataclass(frozen=True) |
| class Topology: |
| """The untrainable wiring: tier geometry plus the fixed structural plan. |
| |
| The frontier-track training experiment treats this as fixed and learns the |
| initializer-filled constants. Nothing here depends on a particular (a,b,p). |
| """ |
|
|
| geom: TierGeometry |
|
|
| @property |
| def n(self) -> int: |
| return self.geom.n |
|
|
| @property |
| def k(self) -> int: |
| return self.geom.k |
|
|
| |
| @property |
| def prod_col_width(self) -> int: |
| n = self.n |
| return (2 * LIMB_BITS) + max(1, (n).bit_length() + (2 * LIMB_BITS * n).bit_length()) |
|
|
|
|
| def build_topology(tier_idx: int, max_bits: int) -> Topology: |
| return Topology(TierGeometry(tier_idx=tier_idx, max_bits=max_bits)) |
|
|
|
|
| class Initializer: |
| """Fills the circuit's learnable constants. Constructed or random.""" |
|
|
| def step_one(self) -> float: |
| raise NotImplementedError |
|
|
| def gate_base(self) -> float: |
| raise NotImplementedError |
|
|
|
|
| class ConstructedInit(Initializer): |
| """Bit-exact arithmetic-circuit constants (the +-1 / 2^16 grid).""" |
|
|
| def step_one(self) -> float: |
| return 1.0 |
|
|
| def gate_base(self) -> float: |
| return float(B) |
|
|
|
|
| class RandomInit(Initializer): |
| """Same slots, seeded random content: the constructed weights become one |
| initializer among several for the trainable topology.""" |
|
|
| def __init__(self, seed: int = 0): |
| g = torch.Generator().manual_seed(seed) |
| self._a = float(torch.rand((), generator=g)) |
| self._b = float(torch.rand((), generator=g)) * B |
|
|
| def step_one(self) -> float: |
| return self._a |
|
|
| def gate_base(self) -> float: |
| return self._b |
|
|
|
|
| |
| |
| |
|
|
| class ModmulCircuit(nn.Module): |
| """Exact ``(a*b) mod p`` as a linear + ReLU circuit at a tier geometry. |
| |
| The constants the initializer fills are registered as float32 buffers so |
| they save to safetensors on the exact weight grid; the forward pass casts |
| to float64 for exact integer arithmetic. With :class:`ConstructedInit` |
| these buffers are ``1.0`` and ``2^16``; randomising them collapses |
| correctness, which is the operational test that the capability lives in the |
| weights rather than in the wiring. |
| |
| Inputs to :meth:`forward` are the per-problem preprocessed tensors (limbs |
| and bits), exactly what a competition ``predict_digits`` would compute |
| outside the network. No (a,b,p) integer arithmetic happens in forward. |
| """ |
|
|
| def __init__(self, topology: Topology, init: Initializer | None = None): |
| super().__init__() |
| self.topology = topology |
| init = init or ConstructedInit() |
| self.register_buffer( |
| "step_one", torch.tensor(init.step_one(), dtype=torch.float32) |
| ) |
| self.register_buffer( |
| "gate_base", torch.tensor(init.gate_base(), dtype=torch.float32) |
| ) |
|
|
| @property |
| def n(self) -> int: |
| return self.topology.n |
|
|
| @property |
| def k(self) -> int: |
| return self.topology.k |
|
|
| def _consts(self) -> Consts: |
| """The learnable constants as a plain dataclass for the primitives.""" |
| return Consts(one=float(self.step_one), base=float(self.gate_base)) |
|
|
| |
|
|
| def _mul( |
| self, |
| x_limbs: torch.Tensor, |
| y_bits: torch.Tensor, |
| consts: Consts, |
| peak: list | None = None, |
| ) -> torch.Tensor: |
| return schoolbook_mul(x_limbs, y_bits, peak=peak, consts=consts) |
|
|
| def forward( |
| self, |
| x_limbs: torch.Tensor, |
| y_bits: torch.Tensor, |
| p_bits: torch.Tensor, |
| mu_bits: torch.Tensor, |
| trace: dict | None = None, |
| ) -> torch.Tensor: |
| """Compute base-2^16 limbs of ``(x*y) mod p``. |
| |
| Args: |
| x_limbs: ``(..., n)`` base-2^16 limbs of ``x = a mod p`` in ``[0, p)``. |
| y_bits: ``(..., 16n)`` bits of ``y = b mod p`` (LSB first). |
| p_bits: ``(..., 16n)`` bits of ``p`` (LSB first). |
| mu_bits: ``(..., 16(n+1)+1)`` bits of ``mu = floor(2^(32n)/p)``. |
| |
| Returns ``(..., n)`` limbs of the residue, all ``< 2^16``, value ``< p``. |
| """ |
| x_limbs = x_limbs.double() |
| y_bits = y_bits.double() |
| p_bits = p_bits.double() |
| mu_bits = mu_bits.double() |
| n, k = self.n, self.k |
| c = self._consts() |
| peak_a: list = [] |
| peak_b: list = [] |
| peak_c: list = [] |
|
|
| |
| t = self._mul(x_limbs, y_bits, c, peak=peak_a) |
| t = self._fit(t, 2 * n + 1) |
|
|
| |
| q1 = t[..., (k - 1):] |
| q1_bits = self._limbs_to_bits(q1, c) |
| mu_lo = mu_bits[..., : (LIMB_BITS * (k + 1) + 1)] |
| |
| mu_limbs = self._bits_to_limbs(mu_lo) |
| q2 = schoolbook_mul(mu_limbs, q1_bits, peak=peak_b, consts=c) |
| |
| q3 = q2[..., (k + 1):] |
| q3 = self._fit(q3, n + 1) |
|
|
| |
| p_limbs = self._bits_to_limbs(p_bits) |
| q3_bits = self._limbs_to_bits(q3, c) |
| qp = schoolbook_mul(p_limbs, q3_bits, peak=peak_c, consts=c) |
| qp_lo = self._fit(qp[..., : (k + 1)], k + 1) |
| t_lo = self._fit(t[..., : (k + 1)], k + 1) |
|
|
| |
| r = self._sub_limbs(t_lo, qp_lo, k + 1, c) |
|
|
| |
| p_lo = self._fit(p_limbs, k + 1) |
| for _ in range(2): |
| ge = self._ge(r, p_lo, k + 1, c) |
| r = self._cond_sub(r, p_lo, ge, k + 1, c) |
|
|
| if trace is not None: |
| trace["peak_mul_xy"] = max(peak_a, default=0.0) |
| trace["peak_mul_q1mu"] = max(peak_b, default=0.0) |
| trace["peak_mul_q3p"] = max(peak_c, default=0.0) |
| trace["peak_overall"] = max( |
| trace["peak_mul_xy"], trace["peak_mul_q1mu"], trace["peak_mul_q3p"] |
| ) |
| return self._fit(r[..., :n], n) |
|
|
| |
|
|
| @staticmethod |
| def _fit(limbs: torch.Tensor, width: int) -> torch.Tensor: |
| """Pad/truncate a limb vector to ``width`` limbs (linear selection).""" |
| cur = limbs.shape[-1] |
| if cur == width: |
| return limbs |
| if cur > width: |
| return limbs[..., :width] |
| pad = torch.zeros( |
| (*limbs.shape[:-1], width - cur), dtype=limbs.dtype |
| ) |
| return torch.cat([limbs, pad], dim=-1) |
|
|
| @staticmethod |
| def _limbs_to_bits(limbs: torch.Tensor, consts: Consts) -> torch.Tensor: |
| """Each base-2^16 limb -> 16 bits (LSB first), concatenated.""" |
| parts = [ |
| peel_bits(limbs[..., i], LIMB_BITS, consts) |
| for i in range(limbs.shape[-1]) |
| ] |
| return torch.cat(parts, dim=-1) |
|
|
| @staticmethod |
| def _bits_to_limbs(bits: torch.Tensor) -> torch.Tensor: |
| """Group bits (LSB first) into base-2^16 limb values (linear).""" |
| nbits = bits.shape[-1] |
| nl = (nbits + LIMB_BITS - 1) // LIMB_BITS |
| w = torch.tensor([float(1 << j) for j in range(LIMB_BITS)], dtype=bits.dtype) |
| out = [] |
| for i in range(nl): |
| chunk = bits[..., i * LIMB_BITS : (i + 1) * LIMB_BITS] |
| if chunk.shape[-1] < LIMB_BITS: |
| pad = torch.zeros( |
| (*chunk.shape[:-1], LIMB_BITS - chunk.shape[-1]), dtype=bits.dtype |
| ) |
| chunk = torch.cat([chunk, pad], dim=-1) |
| out.append((chunk * w).sum(dim=-1)) |
| return torch.stack(out, dim=-1) |
|
|
| def _sub_limbs( |
| self, a: torch.Tensor, b: torch.Tensor, width: int, consts: Consts |
| ) -> torch.Tensor: |
| """``a - b`` as base-2^16 limbs, assuming ``a >= b`` (borrow ripple). |
| |
| Borrow is a step comparator; the difference is recombined linearly. |
| """ |
| a = self._fit(a, width) |
| b = self._fit(b, width) |
| borrow = torch.zeros(a.shape[:-1], dtype=a.dtype) |
| out = [] |
| for i in range(width): |
| d = a[..., i] - b[..., i] - borrow |
| need = step_gate(-d, 1.0, consts.one) |
| d = d + need * consts.base |
| borrow = need |
| out.append(d) |
| return torch.stack(out, dim=-1) |
|
|
| def _ge( |
| self, a: torch.Tensor, b: torch.Tensor, width: int, consts: Consts |
| ) -> torch.Tensor: |
| """Bit ``1{a >= b}`` for base-2^16 limb vectors via borrow-out.""" |
| a = self._fit(a, width) |
| b = self._fit(b, width) |
| borrow = torch.zeros(a.shape[:-1], dtype=a.dtype) |
| for i in range(width): |
| d = a[..., i] - b[..., i] - borrow |
| borrow = step_gate(-d, 1.0, consts.one) |
| return 1.0 - borrow |
|
|
| def _cond_sub( |
| self, |
| r: torch.Tensor, |
| p: torch.Tensor, |
| cond: torch.Tensor, |
| width: int, |
| consts: Consts, |
| ) -> torch.Tensor: |
| """``r - cond*p`` (cond a bit): gate p's limbs, then borrow-subtract.""" |
| p = self._fit(p, width) |
| gated = torch.stack( |
| [gated_product(p[..., i], cond, consts.base) for i in range(width)], |
| dim=-1, |
| ) |
| diff = self._sub_limbs(r, gated, width, consts) |
| |
| keep = (1.0 - cond).unsqueeze(-1) |
| take = cond.unsqueeze(-1) |
| return self._fit(r, width) * keep + diff * take |
|
|
|
|
| |
| |
| |
| |
| |
|
|
| def preprocess(a: int, b: int, p: int, geom: TierGeometry): |
| """Build (x_limbs, y_bits, p_bits, mu_bits) for one problem. |
| |
| ``x = a mod p`` as limbs, ``y = b mod p`` as bits, ``p`` as bits, and |
| ``mu = floor(2^(32n)/p)`` as bits. All integer work here is caller-side |
| representation, never inside :meth:`ModmulCircuit.forward`. |
| """ |
| n = geom.n |
| x = a % p |
| y = b % p |
| mu = (1 << (2 * LIMB_BITS * n)) // p |
| x_limbs = torch.tensor(int_to_limbs(x, n), dtype=torch.float64) |
| y_bits = torch.tensor(int_to_bits(y, LIMB_BITS * n), dtype=torch.float64) |
| p_bits = torch.tensor(int_to_bits(p, LIMB_BITS * n), dtype=torch.float64) |
| mu_bits = torch.tensor( |
| int_to_bits(mu, LIMB_BITS * (n + 1) + 1), dtype=torch.float64 |
| ) |
| return x_limbs, y_bits, p_bits, mu_bits |
|
|
|
|
| def preprocess_batch(triples, geom: TierGeometry): |
| """Stack :func:`preprocess` over a list of ``(a, b, p)`` into batched tensors.""" |
| xl, yb, pb, mb = [], [], [], [] |
| for a, b, p in triples: |
| a4, b4, c4, d4 = preprocess(a, b, p, geom) |
| xl.append(a4) |
| yb.append(b4) |
| pb.append(c4) |
| mb.append(d4) |
| return ( |
| torch.stack(xl), |
| torch.stack(yb), |
| torch.stack(pb), |
| torch.stack(mb), |
| ) |
|
|
|
|
| def run_one(circuit: ModmulCircuit, a: int, b: int, p: int) -> int: |
| """Convenience: preprocess, forward, decode to an integer residue.""" |
| xl, yb, pb, mb = preprocess(a, b, p, circuit.topology.geom) |
| out = circuit(xl, yb, pb, mb) |
| return limbs_to_int(out) |
|
|
|
|
| |
| |
| |
|
|
| def save_circuit(circuit: ModmulCircuit, path) -> None: |
| """Write the circuit's filled constants to safetensors (float32 storage). |
| |
| The topology (tier id, max_bits) travels in the safetensors metadata so a |
| load reconstructs the same wiring before refilling the weights. |
| """ |
| from pathlib import Path |
|
|
| from safetensors.torch import save_file |
|
|
| path = Path(path) |
| tensors = {k: v.contiguous() for k, v in circuit.state_dict().items()} |
| meta = { |
| "tier_idx": str(circuit.topology.geom.tier_idx), |
| "max_bits": str(circuit.topology.geom.max_bits), |
| } |
| save_file(tensors, str(path), metadata=meta) |
|
|
|
|
| def load_circuit(path) -> ModmulCircuit: |
| """Reconstruct topology from metadata, then load the stored constants.""" |
| from pathlib import Path |
|
|
| from safetensors import safe_open |
| from safetensors.torch import load_file |
|
|
| path = Path(path) |
| with safe_open(str(path), framework="pt") as f: |
| meta = f.metadata() or {} |
| topo = build_topology(int(meta["tier_idx"]), int(meta["max_bits"])) |
| circuit = ModmulCircuit(topo) |
| circuit.load_state_dict(load_file(str(path))) |
| circuit.eval() |
| return circuit |
|
|
|
|
|
|