Skip to main content

Bring your own model

The training loop is architecture-neutral. A custom model must follow the batch and loss protocols and be constructible through a registry factory.

Step 1: Follow the batch contract​

Dictionary batches are passed as keyword arguments. The bundled loader emits:

input_ids
labels
attention_mask

A tuple/list batch is passed as:

model(batch[0], batch[1])

Step 2: Return a usable loss​

The model can return:

loss_tensor
{"loss": loss_tensor, "auxiliary": ...}
{"logits": logits, ...}

or a tuple/list whose first element is a loss or compatible 3-D logits.

A bare tensor is always interpreted as a loss, not logits.

Step 3: Register a factory​

import torch
from torch import nn
from torch.nn import functional as F

from speedtronic import register_model


class SmallCausalLM(nn.Module):
def __init__(self, vocab_size: int, d_model: int):
super().__init__()
self.embedding = nn.Embedding(vocab_size, d_model)
self.output = nn.Linear(d_model, vocab_size)

def forward(
self,
input_ids,
labels=None,
attention_mask=None,
):
logits = self.output(self.embedding(input_ids))
if labels is None:
return {"logits": logits}

if labels.shape[1] == input_ids.shape[1]:
labels = labels[:, 1:]

loss = F.cross_entropy(
logits[:, :-1].reshape(-1, logits.shape[-1]),
labels.reshape(-1),
ignore_index=-100,
)
return {"logits": logits, "loss": loss}


@register_model("small_causal_lm")
def make_small_causal_lm(**kwargs):
return SmallCausalLM(
vocab_size=kwargs["vocab_size"],
d_model=kwargs["d_model"],
)

Factories receive the standard model fields even when they are unrelated to the custom architecture. Accepting **kwargs avoids accidental construction errors.

Step 4: Train programmatically​

from speedtronic.runtime import train_from_config

result = train_from_config(
{
"run": {
"max_steps": 10,
"device": "cpu",
"output_dir": "runs/small-lm",
},
"model": {
"name": "small_causal_lm",
"vocab_size": 128,
"max_seq_len": 32,
"n_layer": 2,
"n_head": 4,
"n_kv_head": 2,
"d_model": 64,
},
"data": {
"block_size": 32,
"micro_batch_size": 2,
"target_batch_size": 4,
},
"scheduler": {"warmup_steps": 1, "max_steps": 10},
"precision": {"mode": "fp32"},
}
)

CLI limitation​

Registry entries are in-memory. A separate speedtronic train --config ... process does not import arbitrary application registration code. YAML can select a registered name, but it cannot discover a Python factory by itself.

For a CLI-visible custom model, add an application-owned import/plugin mechanism and construct the runtime after registration.

Non-language-model model​

A model can ignore causal keys by accepting them explicitly and returning any differentiable scalar loss:

class TinyRegressor(nn.Module):
def __init__(self):
super().__init__()
self.value = nn.Parameter(torch.zeros(()))

def forward(self, input_ids, labels=None, attention_mask=None):
if labels is None:
raise ValueError("labels are required")
return (self.value - labels.float().mean()) ** 2

The built-in collator still produces language-model-shaped batches, so use a custom DataLoader for other batch structures.

Direct Trainer injection​

For complete control:

from speedtronic.trainer import Trainer

trainer = Trainer(
model,
optimizer,
dataloader,
device="cpu",
max_steps=100,
)
result = trainer.fit()

Gradient checkpointing hook​

class MyModule(nn.Module):
def set_gradient_checkpointing(self, enabled: bool = True):
self.use_checkpointing = bool(enabled)

The trainer warns rather than fails if the hook is absent or raises.

Model-returned metrics​

The trainer currently discards additional output-dictionary metrics. If metrics are required, send them through a hook, a global collector, or a custom Trainer implementation.

Checklist​

  • Model accepts the actual batch keys.
  • Output begins with a differentiable loss under the trainer's interpretation.
  • Factory accepts standard registry keywords.
  • Registration occurs in the same process as runtime construction.
  • Model context and data block sizes are compatible.
  • Custom batch shapes have a matching collator.
  • Gradient checkpointing exposes the optional hook.

Related: Extensions, Data, and Runtime and Trainer.