Per-task LoRA checkpoints from a symmetry-transport hypernetwork

Twenty trained checkpoints in one PyTorch file, one per synthetic task. The hypernetwork generates a rank-two LoRA head per task, then that head is trained and frozen. Base and adapter both ran 600 steps in float32 on a CUDA GPU.

These are synthetic numbers. No language model and no real dataset is involved, and the .pt files are neither Transformers nor PEFT adapters.

Files

  • per_task_lora_pack.pt: all 20 checkpoints, with per-task accuracy and validation BCE.
  • manifest.json: SHA-256 of every checkpoint in the training output, plus held-out numbers.

The model definition isn't a separate file. It's inlined in the usage section below, so everything you need is in this repository.

What the tasks are

Every task is binary classification in 2D. Each example has an input x of shape [batch, 2] and a context c of shape [batch, 3].

The first two context columns are a unit direction (n_x, n_y), the third is a threshold tau. The input rotates into that frame first, so u is the component of x along the direction and v the component across it. Label is 1 when the family's margin is non-negative.

Only the margin differs between families:

Task family Margin
linear u - tau
curved u - tau + 0.55 * (v**2 - 1/3) + 0.4 * sin(pi * tau) * v
axis the curved margin plus 0.6 * x[0] * x[1]
high_frequency u - tau + 0.25 * (v**2 - 1/3) + 0.30 * sin(4 * pi * v)

linear is one straight boundary. curved bends it with a quadratic in v and tilts it by tau. axis adds a term in the unrotated coordinates, which is the whole point of the family: the boundary no longer exists in the (u, v) frame, so the model has to transport the context somewhere the rule does apply. high_frequency is the control, oscillating four times faster in v, which 8 hidden units struggle to fit.

-task-11 through -task-15 are five seeds of one rule, not five rules. Each seed draws its own points (seed = 101 + 1009 * task_seed) over the same grid of 8 orientations by 5 thresholds. Four families times five seeds gives the 20.

Results

Test accuracy per family, mean over five seeds:

Task family Mean test accuracy Sample SD
Linear 99.23% 0.42
Curved 98.10% 0.27
Axis 97.09% 0.42
High-frequency 89.45% 0.89

Checkpoint selection used validation BCE. high_frequency is the hardest family by design; it's the control showing where this breaks.

One 295-parameter model trained jointly on all 20 tasks did not match the per-task checkpoints on axis. The checkpoints stayed separate rather than being distilled or averaged together.

Format

Target is a 2-8-1 tanh MLP. Each checkpoint holds 295 trainable parameters: 173 in the canonical base hypernetwork, 122 in the generated rank-two LoRA head.

per_task_lora_pack.pt is a dictionary under the key tasks, keyed by task name. Each entry carries a flat state_dict (bias, u, v, a, base_generator.*) plus family, task_seed, task_id, parameters, test, and validation BCE. Nothing is shared or dropped, so the pack costs exactly what 20 separate checkpoints cost, as one download.

Usage

Each entry under tasks holds a flat state_dict, which is a plain dict of tensor names to tensors. This snippet rebuilds the model and loads one task:

import torch

H, R, P = 8, 2, 33


class TaskLoRA(torch.nn.Module):
    def __init__(self):
        super().__init__()
        q = 2 * H * R + 3 * R
        self.bias = torch.nn.Parameter(torch.zeros(q))
        self.u = torch.nn.Parameter(torch.zeros(q, 2))
        self.v = torch.nn.Parameter(torch.zeros(2, 3))
        self.a = torch.nn.Parameter(torch.zeros(2))
        g = self.base_generator = torch.nn.Module()
        g.b = torch.nn.Parameter(torch.zeros(P))
        g.u = torch.nn.Parameter(torch.zeros(P, 4))
        g.v = torch.nn.Parameter(torch.zeros(4, 1))
        g.a = torch.nn.Parameter(torch.zeros(4))

    def forward(self, c, x):
        g = self.base_generator
        z = self.bias + torch.tanh(c @ self.v.T + self.a) @ self.u.T
        end = H * R
        a1 = z[:, :end].reshape(-1, H, R)
        b1 = z[:, end:end + 2 * R].reshape(-1, R, 2)
        a2 = z[:, end + 2 * R:end + 2 * R + end].reshape(-1, H, R)
        b2 = z[:, end + 2 * R + end:].reshape(-1, R, 1)

        theta = g.b + torch.tanh(c[:, 2:3] @ g.v.T + g.a) @ g.u.T
        w, b, o, d = (theta[:, :16].reshape(-1, H, 2), theta[:, 16:24],
                      theta[:, 24:32], theta[:, 32])

        n = c[:, :2]
        rot = torch.stack((n[:, 0], n[:, 1], -n[:, 1], n[:, 0]), -1).reshape(-1, 2, 2)
        uv = torch.bmm(rot, x[..., None]).squeeze(-1)
        o = o + torch.bmm(a2, b2).squeeze(-1)
        return (o * torch.tanh(torch.einsum("bhd,bd->bh", w, uv)
                               + torch.einsum("bhd,bd->bh", torch.bmm(a1, b1), x)
                               + b)).sum(1) + d


pack = torch.load("per_task_lora_pack.pt", map_location="cpu", weights_only=False)
task = pack["tasks"]["axis-task-11"]

model = TaskLoRA().double()
model.load_state_dict({k: v.double() for k, v in task["state_dict"].items()}, strict=True)
model.eval()

print("parameters:", sum(p.numel() for p in model.parameters()))
print("stored test accuracy:", task["test"]["accuracy"])

Expected output:

parameters: 295
stored test accuracy: 96.875

Call model(c, x) with x of shape [batch, 2] and c of shape [batch, 3] to get one logit per example. The first two context columns are the direction (n_x, n_y) and the third is tau.

Provenance

Author: NSC64.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. ๐Ÿ™‹ Ask for provider support