Module 13. Supervised Fine-Tuning and Data Work
Read the explanations and code in the existing notebooks, or download the files. Code does not run on this site.
Download course files (.zip) 2588.1 KBThe archive preserves the source folder structure, both languages, and supporting files.
Notebook 13. SFT data: from a dialogue to a correctly packed batch
Main module notebook
Download .ipynb module_13_notebook_sft.ipynb · 58.9 KB
Show notebook text
Notebook 13. SFT data: from a dialogue to a correctly packed batch
Module 13 — hands-on notebook
This is an engineering-disciplined educational SFT data pipeline. One small dialogue crosses the boundaries most often broken in a real collator:
messages → input_ids/labels → shifted causal loss → FFD assignments → packed sequence →
position_ids + segment_ids + cu_seq_lens → block-diagonal causal isolation → padded batch.
We then reproduce the compute, best-of-N, and LoRA calculations and train a tiny causal model, generate held-out answers, and reload its weights. It does not train a large model or reproduce a FlashAttention kernel or distributed trainer; those layers are unnecessary for verifying the data contracts.
Colab contract: Runtime → Run all completes the mandatory path on CPU in a clean
session. The notebook reads no adjacent files and needs no manual upload, API key, or paid
service. A frozen Module-13 numerical payload is embedded with SHA-256 verification.
Route:
-
a chat template and real token labels rather than a
1sentinel; -
the causal-LM shift and
ignore_index=-100; -
multi-turn and tool-response masking;
-
FFD with original indices rather than bin loads only;
-
packed
input_ids,labels,position_ids,segment_ids, andcu_seq_lens; -
a negative cross-example leakage test;
-
a padded batch and padding/loss masks;
-
compute, best-of-N, and strict GiB/MiB units in the LoRA estimate.
import importlib.util
import subprocess
import sys
if importlib.util.find_spec("numpy") is None:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "numpy>=1.24"])
from dataclasses import dataclass
import hashlib
import json
import math
from typing import Iterable, Sequence
import numpy as np
np.set_printoptions(precision=4, suppress=True, linewidth=120)
IGNORE_INDEX = -100
print('Environment ready.')0. Embedded Module-13 numerical contract
The reference numbers belong to the module rather than mutable lecture prose. The JSON is embedded and verified by SHA-256, so future lecture edits cannot change the computational route.
REFERENCE_JSON = '{\n "best_of_n": {\n "coverage": {\n "1": 0.30000000000000004,\n "16": 0.9966767069430399,\n "2": 0.51,\n "4": 0.7599,\n "8": 0.94235199\n },\n "pass_probability": 0.3\n },\n "compute": {\n "accelerator_flops": 990000000000000.0,\n "average_sft_tokens": 1200,\n "mfu": 0.4,\n "n_params": 8030000000.0,\n "pretrain_flops": 7.227e+23,\n "pretrain_tokens": 15000000000000.0,\n "ratio": 20833.333333333336,\n "sft_epochs": 3,\n "sft_examples": 200000,\n "sft_flops": 3.46896e+19,\n "sft_hours": 24.333333333333332,\n "sft_tokens": 240000000\n },\n "lora": {\n "d_kv": 1024,\n "d_model": 4096,\n "four_bit_payload_gib": 3.7392601370811462,\n "fraction": 0.001697570112079701,\n "full_training_states_gib": 119.65632438659668,\n "layers": 32,\n "lora_adapter_bf16_mib": 26.0,\n "lora_training_states_mib": 208.0,\n "params": 13631488,\n "per_layer": 425984,\n "qlora_parameter_floor_gib": 3.9423851370811462,\n "rank": 16,\n "state_saving": 589.0772892878606\n },\n "packing": {\n "bins": [\n {\n "indices": [\n 7,\n 10,\n 18\n ],\n "lengths": [\n 1347,\n 626,\n 72\n ],\n "used": 2045\n },\n {\n "indices": [\n 15,\n 11,\n 1,\n 3\n ],\n "lengths": [\n 754,\n 556,\n 527,\n 180\n ],\n "used": 2017\n },\n {\n "indices": [\n 23,\n 12,\n 6,\n 0,\n 8\n ],\n "lengths": [\n 514,\n 443,\n 425,\n 403,\n 259\n ],\n "used": 2044\n },\n {\n "indices": [\n 14,\n 21,\n 2,\n 4,\n 17,\n 9,\n 13,\n 20\n ],\n "lengths": [\n 392,\n 326,\n 315,\n 267,\n 267,\n 230,\n 174,\n 76\n ],\n "used": 2047\n },\n {\n "indices": [\n 5,\n 22,\n 19,\n 16\n ],\n "lengths": [\n 165,\n 128,\n 126,\n 120\n ],\n "used": 539\n }\n ],\n "ffd_utilization": 0.848828125,\n "forward_reduction": 4.8,\n "lengths": [\n 403,\n 527,\n 315,\n 180,\n 267,\n 165,\n 425,\n 1347,\n 259,\n 230,\n 626,\n 556,\n 443,\n 174,\n 392,\n 754,\n 120,\n 267,\n 72,\n 126,\n 76,\n 326,\n 128,\n 514\n ],\n "max_length": 2048,\n "n_ffd": 5,\n "n_naive": 24,\n "naive_utilization": 0.17683919270833334\n },\n "s1": {\n "accelerators": 8,\n "flops": 1.728e+18,\n "minutes": 9.090909090909092,\n "n_params": 32000000000.0,\n "tokens": 9000000,\n "tokens_per_trace": 9000,\n "traces": 1000\n },\n "schema_version": 3,\n "template": {\n "content_lengths": {\n "assistant": 20,\n "system": 12,\n "user": 8\n },\n "per_message_overhead": 5,\n "special_ids": {\n "bos": 1,\n "end_header": 3,\n "eot": 4,\n "newline": 5,\n "pad": 0,\n "role_assistant": 8,\n "role_system": 6,\n "role_tool": 9,\n "role_user": 7,\n "start_header": 2\n },\n "total_tokens": 56,\n "trainable_fraction": 0.375,\n "trainable_tokens": 21\n }\n}'
EXPECTED_SHA256 = "ff1e8f3e598ed6fbe3e6b807fb4d3f783cad366dc965b1f51681b3ebef6bb840"
actual_sha256 = hashlib.sha256((REFERENCE_JSON + "\n").encode("utf-8")).hexdigest()
assert actual_sha256 == EXPECTED_SHA256, (actual_sha256, EXPECTED_SHA256)
REF = json.loads(REFERENCE_JSON)
assert REF["schema_version"] == 3
print('Module-13 reference payload verified', actual_sha256[:12])1. Chat template: input_ids and −100 labels
The teaching template preserves the lecture arithmetic: BOS; four header tokens per
message; content; EOT. For assistant messages the content and its EOT are trainable.
System, user, tool, and every header receive −100. In a real causal LM, labels contain
source token IDs rather than a 1 sentinel meaning "train here."
@dataclass(frozen=True)
class Message:
"""A pre-tokenized chat message used by the teaching template."""
role: str
token_ids: tuple[int, ...]
@dataclass(frozen=True)
class SFTExample:
"""One causal-LM example before packing; all arrays have shape [tokens]."""
input_ids: np.ndarray
labels: np.ndarray
attention_mask: np.ndarray
position_ids: np.ndarray
message_spans: tuple[tuple[str, int, int], ...]
def make_tokens(length: int, start: int) -> tuple[int, ...]:
"""Create deterministic content token IDs for a small offline example."""
if length < 0:
raise ValueError("length must be non-negative")
return tuple(range(start, start + length))
def build_sft_example(
messages: Sequence[Message],
special_ids: dict[str, int],
trainable_roles: frozenset[str] = frozenset({"assistant"}),
) -> SFTExample:
"""Encode messages and mask every non-trainable role with IGNORE_INDEX.
Contract:
- the sequence begins with BOS;
- every message has four header tokens and one closing EOT token;
- labels equal token IDs on trainable content plus its EOT;
- headers and non-trainable roles remain IGNORE_INDEX.
"""
role_token = {
"system": special_ids["role_system"],
"user": special_ids["role_user"],
"assistant": special_ids["role_assistant"],
"tool": special_ids["role_tool"],
}
input_ids: list[int] = [special_ids["bos"]]
labels: list[int] = [IGNORE_INDEX]
spans: list[tuple[str, int, int]] = []
for message in messages:
if message.role not in role_token:
raise ValueError(f"unsupported role: {message.role}")
if any((not isinstance(token, int)) or isinstance(token, bool) or token < 0 for token in message.token_ids):
raise ValueError("message token_ids must be non-negative integers")
header = [
special_ids["start_header"],
role_token[message.role],
special_ids["end_header"],
special_ids["newline"],
]
body = [*message.token_ids, special_ids["eot"]]
start = len(input_ids)
input_ids.extend(header + body)
labels.extend([IGNORE_INDEX] * len(header))
if message.role in trainable_roles:
labels.extend(body)
else:
labels.extend([IGNORE_INDEX] * len(body))
spans.append((message.role, start, len(input_ids)))
ids = np.asarray(input_ids, dtype=np.int64)
target = np.asarray(labels, dtype=np.int64)
return SFTExample(
input_ids=ids,
labels=target,
attention_mask=np.ones(ids.shape, dtype=bool),
position_ids=np.arange(ids.size, dtype=np.int64),
message_spans=tuple(spans),
)
tm = REF["template"]
special_ids = {name: int(value) for name, value in tm["special_ids"].items()}
lengths = tm["content_lengths"]
messages = [
Message("system", make_tokens(lengths["system"], 100)),
Message("user", make_tokens(lengths["user"], 200)),
Message("assistant", make_tokens(lengths["assistant"], 300)),
]
example = build_sft_example(messages, special_ids)
active = example.labels != IGNORE_INDEX
assert example.input_ids.size == tm["total_tokens"] == 56
assert int(active.sum()) == tm["trainable_tokens"] == 21
assert math.isclose(float(active.mean()), tm["trainable_fraction"], abs_tol=1e-15)
assistant_role, assistant_start, assistant_end = example.message_spans[-1]
assert assistant_role == "assistant"
assert example.labels[assistant_end - 1] == special_ids["eot"] # teach the stop token
assert np.all(example.labels[:assistant_start] == IGNORE_INDEX)
print('Single-turn dialogue: 56 tokens, 21 trainable (37.5%)')
print("trainable positions:", np.flatnonzero(active)[:4], "...", np.flatnonzero(active)[-4:])2. What the causal loss predicts
Logits at position t predict token t+1. Many library models perform this shift inside
forward. Here the caller already supplies logits for the first T−1 positions; the
function pairs those rows with labels[1:] and excludes −100 targets. Do not trim the logits again inside the function. Changing logits on masked rows must not change the loss.
def masked_next_token_nll(logits: np.ndarray, labels: np.ndarray) -> tuple[float, np.ndarray, np.ndarray]:
"""Return mean shifted causal NLL over targets not equal to IGNORE_INDEX.
`logits[t]` predicts `labels[t+1]`. The implementation uses a stable log-sum-exp and
rejects invalid active target IDs rather than silently wrapping them.
"""
scores = np.asarray(logits, dtype=np.float64)
target = np.asarray(labels)
if target.dtype.kind not in "iu":
raise ValueError("labels must be integer token IDs or IGNORE_INDEX")
if not np.isfinite(scores).all():
raise ValueError("logits must be finite")
if scores.ndim != 2 or target.ndim != 1 or scores.shape[0] != target.size - 1:
raise ValueError("logits must have shape [len(labels)-1, vocab_size]")
shifted_target = target[1:]
active = shifted_target != IGNORE_INDEX
if not np.any(active):
raise ValueError("at least one next-token target must be trainable")
active_target = shifted_target[active]
if np.any(active_target < 0) or np.any(active_target >= scores.shape[1]):
raise ValueError("an active target ID is outside the vocabulary")
active_scores = scores[active]
row_max = active_scores.max(axis=1)
log_partition = row_max + np.log(np.exp(active_scores - row_max[:, None]).sum(axis=1))
token_nll = log_partition - active_scores[np.arange(active_target.size), active_target]
return float(token_nll.mean()), token_nll, active
rng = np.random.default_rng(11)
vocab_size = int(example.input_ids.max()) + 32
logits = rng.normal(0.0, 0.5, size=(example.input_ids.size - 1, vocab_size))
shifted_target = example.labels[1:]
active_rows = shifted_target != IGNORE_INDEX
for row, token_id in zip(np.flatnonzero(active_rows), shifted_target[active_rows]):
logits[row, token_id] += 5.0
loss, token_nll, active_rows = masked_next_token_nll(logits, example.labels)
corrupted_masked_rows = logits.copy()
corrupted_masked_rows[~active_rows] = 1e6 * rng.normal(size=corrupted_masked_rows[~active_rows].shape)
loss_after_corruption, _, _ = masked_next_token_nll(corrupted_masked_rows, example.labels)
assert math.isclose(loss, loss_after_corruption, rel_tol=0.0, abs_tol=1e-12)
assert token_nll.size == tm["trainable_tokens"]
print('Shifted NLL uses assistant targets only; masked rows do not matter', f"mean NLL={loss:.4f}")3. Multi-turn and tool use
The assistant tool call and final assistant answer are trainable; the tool result is masked. This is not a special loss case: assign roles correctly before creating labels.
conversation = [
Message("system", make_tokens(4, 100)),
Message("user", make_tokens(3, 200)),
Message("assistant", make_tokens(5, 300)), # tool call
Message("tool", make_tokens(4, 400)), # tool result: context in this objective
Message("assistant", make_tokens(6, 500)), # final answer
]
multi = build_sft_example(conversation, special_ids)
active = multi.labels != IGNORE_INDEX
assistant_expected = (5 + 1) + (6 + 1)
assert int(active.sum()) == assistant_expected
for role, start, end in multi.message_spans:
if role == "tool":
assert np.all(multi.labels[start:end] == IGNORE_INDEX)
elif role == "assistant":
# Four header tokens are masked; content and EOT are active.
assert np.all(multi.labels[start:start + 4] == IGNORE_INDEX)
assert np.all(multi.labels[start + 4:end] == multi.input_ids[start + 4:end])
print('Two assistant segments train; the tool response is fully masked', f"active targets={active.sum()}")4. FFD must return assignments
Bin loads alone are insufficient: the collator must know which examples occupy each bin. The implementation preserves original indices, lengths, used tokens, and utilization. Sorting is stable: equal lengths retain increasing original indices.
@dataclass(frozen=True)
class BinAssignment:
"""One deterministic FFD bin with traceable source examples."""
example_indices: tuple[int, ...]
lengths: tuple[int, ...]
used: int
capacity: int
@property
def utilization(self) -> float:
"""Return used/capacity for the bin."""
return self.used / self.capacity
def first_fit_decreasing(lengths: Sequence[int], max_length: int) -> list[BinAssignment]:
"""Pack lengths with stable FFD and preserve original example indices."""
if not isinstance(max_length, int) or isinstance(max_length, bool) or max_length <= 0:
raise ValueError("max_length must be a positive integer")
normalized: list[int] = []
for value in lengths:
if not isinstance(value, (int, np.integer)) or isinstance(value, bool) or value <= 0:
raise ValueError("every length must be a positive integer")
if int(value) > max_length:
raise ValueError(f"length {value} exceeds max_length={max_length}")
normalized.append(int(value))
mutable: list[dict[str, object]] = []
for index in sorted(range(len(normalized)), key=lambda i: (-normalized[i], i)):
length = normalized[index]
for bucket in mutable:
if int(bucket["used"]) + length <= max_length:
bucket["indices"].append(index)
bucket["lengths"].append(length)
bucket["used"] = int(bucket["used"]) + length
break
else:
mutable.append({"indices": [index], "lengths": [length], "used": length})
return [
BinAssignment(tuple(b["indices"]), tuple(b["lengths"]), int(b["used"]), max_length)
for b in mutable
]
pk = REF["packing"]
assignments = first_fit_decreasing(pk["lengths"], pk["max_length"])
assert [list(b.example_indices) for b in assignments] == [b["indices"] for b in pk["bins"]]
assert [b.used for b in assignments] == [b["used"] for b in pk["bins"]]
assert len(assignments) == pk["n_ffd"] == 5
naive_util = sum(pk["lengths"]) / (len(pk["lengths"]) * pk["max_length"])
ffd_util = sum(pk["lengths"]) / (len(assignments) * pk["max_length"])
assert math.isclose(naive_util, pk["naive_utilization"], abs_tol=1e-15)
assert math.isclose(ffd_util, pk["ffd_utilization"], abs_tol=1e-15)
print('FFD: 24 examples → 5 bins; assignments match the frozen contract')
for i, bucket in enumerate(assignments):
print(f"bin {i}: used={bucket.used:4d}/{bucket.capacity}, indices={bucket.example_indices}")5. The complete packed artifact
Each bin carries:
input_idsand token labels;- an
attention_maskfor valid tokens; position_idsreset to zero at each example boundary;segment_idsfor explicit isolation checks;cu_seq_lensandmax_seqlen, the compact varlen form;- original
example_indicesfor tracing the result back to the dataset.
@dataclass(frozen=True)
class PackedSequence:
"""A traceable packed SFT sequence in both dense and varlen-friendly form."""
input_ids: np.ndarray
labels: np.ndarray
attention_mask: np.ndarray
position_ids: np.ndarray
segment_ids: np.ndarray
cu_seq_lens: np.ndarray
max_seqlen: int
example_indices: tuple[int, ...]
lengths: tuple[int, ...]
def synthetic_example(length: int, example_index: int) -> SFTExample:
"""Create a deterministic variable-length example for packing tests."""
base = 10_000 + example_index * 3_000
ids = np.arange(base, base + length, dtype=np.int64)
labels = ids.copy()
labels[: max(1, length // 3)] = IGNORE_INDEX # a prompt-like masked prefix
return SFTExample(
input_ids=ids,
labels=labels,
attention_mask=np.ones(length, dtype=bool),
position_ids=np.arange(length, dtype=np.int64),
message_spans=(),
)
def pack_examples(examples: Sequence[SFTExample], assignment: BinAssignment) -> PackedSequence:
"""Concatenate one bin while preserving per-example positions and boundaries."""
pieces_ids: list[np.ndarray] = []
pieces_labels: list[np.ndarray] = []
pieces_pos: list[np.ndarray] = []
pieces_seg: list[np.ndarray] = []
cu = [0]
lengths: list[int] = []
for segment, example_index in enumerate(assignment.example_indices):
ex = examples[example_index]
n = int(ex.input_ids.size)
if n != assignment.lengths[segment]:
raise ValueError("assignment length does not match the source example")
if not (ex.labels.shape == ex.attention_mask.shape == ex.position_ids.shape == ex.input_ids.shape):
raise ValueError("all example arrays must have the same [tokens] shape")
if not np.all(ex.attention_mask):
raise ValueError("source examples must be unpadded before packing")
if not np.array_equal(ex.position_ids, np.arange(n)):
raise ValueError("source position_ids must start at zero and be contiguous")
pieces_ids.append(ex.input_ids)
target = ex.labels.copy()
target[0] = IGNORE_INDEX # prevent a next-token target crossing the boundary
pieces_labels.append(target)
pieces_pos.append(ex.position_ids)
pieces_seg.append(np.full(n, segment, dtype=np.int32))
lengths.append(n)
cu.append(cu[-1] + n)
input_ids = np.concatenate(pieces_ids)
labels = np.concatenate(pieces_labels)
position_ids = np.concatenate(pieces_pos)
segment_ids = np.concatenate(pieces_seg)
if input_ids.size != assignment.used:
raise AssertionError("packed size must equal the bin's used count")
return PackedSequence(
input_ids=input_ids,
labels=labels,
attention_mask=np.ones(input_ids.shape, dtype=bool),
position_ids=position_ids,
segment_ids=segment_ids,
cu_seq_lens=np.asarray(cu, dtype=np.int32),
max_seqlen=max(lengths),
example_indices=assignment.example_indices,
lengths=tuple(lengths),
)
source_examples = [synthetic_example(length, i) for i, length in enumerate(pk["lengths"])]
packed = [pack_examples(source_examples, assignment) for assignment in assignments]
for item in packed:
assert item.cu_seq_lens[0] == 0 and item.cu_seq_lens[-1] == item.input_ids.size
assert np.array_equal(np.diff(item.cu_seq_lens), np.asarray(item.lengths))
for boundary in item.cu_seq_lens[:-1]:
assert item.position_ids[int(boundary)] == 0
# Preserve source labels except the explicitly ignored segment start.
for segment, original_index in enumerate(item.example_indices):
lo, hi = item.cu_seq_lens[segment:segment + 2]
assert item.labels[lo] == IGNORE_INDEX
assert np.array_equal(item.labels[lo+1:hi], source_examples[original_index].labels[1:])
print('Packed artifacts preserve labels, boundaries, position resets, and cu_seq_lens')
print("first bin cu_seq_lens:", packed[0].cu_seq_lens, "max_seqlen:", packed[0].max_seqlen)6. Packing without isolation leaks context
A global causal mask allows a later example to read an earlier one. The correct
block-diagonal causal mask requires valid tokens, equal segment IDs, and
key_position ≤ query_position. The full square matrix is pedagogical; a varlen kernel
usually consumes cu_seq_lens and never materializes it.
def block_diagonal_causal_mask(item: PackedSequence) -> np.ndarray:
"""Return [query, key] permission: valid, same segment, and key not in future."""
valid_q = item.attention_mask[:, None]
valid_k = item.attention_mask[None, :]
same_segment = item.segment_ids[:, None] == item.segment_ids[None, :]
causal = item.position_ids[None, :] <= item.position_ids[:, None]
return valid_q & valid_k & same_segment & causal
item = packed[0]
naive = np.tril(np.ones((item.input_ids.size, item.input_ids.size), dtype=bool))
same_segment = item.segment_ids[:, None] == item.segment_ids[None, :]
block = block_diagonal_causal_mask(item)
naive_leaks = int((naive & ~same_segment).sum())
block_leaks = int((block & ~same_segment).sum())
assert naive_leaks > 0 and block_leaks == 0
# Each segment's first query can attend only to its own first key.
for boundary in item.cu_seq_lens[:-1]:
row = block[int(boundary)]
assert int(row.sum()) == 1
assert row[int(boundary)]
print('The global mask leaked across examples; the block-diagonal mask did not', f"naive cross-example cells={naive_leaks:,}; block={block_leaks}")7. A padded batch over packed bins
When a backend does not accept varlen sequences directly, bins can be collated into a
regular [batch, max_length] tensor. Padding receives attention_mask=0, labels=-100,
segment_ids=-1; real tokens and position IDs remain unchanged.
@dataclass(frozen=True)
class PackedBatch:
"""A padded batch of packed bins with explicit masks and trace metadata."""
input_ids: np.ndarray
labels: np.ndarray
attention_mask: np.ndarray
position_ids: np.ndarray
segment_ids: np.ndarray
used_lengths: np.ndarray
def collate_packed_sequences(
sequences: Sequence[PackedSequence],
pad_to: int,
pad_token_id: int,
) -> PackedBatch:
"""Pad packed bins to one dense shape without making padding trainable."""
if pad_to <= 0 or any(item.input_ids.size > pad_to for item in sequences):
raise ValueError("pad_to must fit every packed sequence")
batch = len(sequences)
input_ids = np.full((batch, pad_to), pad_token_id, dtype=np.int64)
labels = np.full((batch, pad_to), IGNORE_INDEX, dtype=np.int64)
attention = np.zeros((batch, pad_to), dtype=bool)
positions = np.zeros((batch, pad_to), dtype=np.int64)
segments = np.full((batch, pad_to), -1, dtype=np.int32)
used = np.zeros(batch, dtype=np.int32)
for row, item in enumerate(sequences):
n = item.input_ids.size
used[row] = n
input_ids[row, :n] = item.input_ids
labels[row, :n] = item.labels
attention[row, :n] = True
positions[row, :n] = item.position_ids
segments[row, :n] = item.segment_ids
return PackedBatch(input_ids, labels, attention, positions, segments, used)
batch = collate_packed_sequences(packed, pk["max_length"], special_ids["pad"])
assert batch.input_ids.shape == (pk["n_ffd"], pk["max_length"])
assert np.array_equal(batch.attention_mask.sum(axis=1), batch.used_lengths)
assert np.all(batch.labels[~batch.attention_mask] == IGNORE_INDEX)
assert np.all(batch.segment_ids[~batch.attention_mask] == -1)
assert np.array_equal(batch.labels != IGNORE_INDEX, (batch.labels != IGNORE_INDEX) & batch.attention_mask)
print('Built a [5, 2048] batch; padding and loss masks agree', "used lengths:", batch.used_lengths.tolist())8. The compute bridge and s1
C = 6ND is a scenario approximation here. H100 throughput and MFU are explicit inputs,
not a wall-clock promise for every trainer.
cp = REF["compute"]
sft_flops = 6 * cp["n_params"] * cp["sft_tokens"] * cp["sft_epochs"]
pretrain_flops = 6 * cp["n_params"] * cp["pretrain_tokens"]
sft_hours = sft_flops / (cp["accelerator_flops"] * cp["mfu"]) / 3600
assert math.isclose(sft_flops, cp["sft_flops"], rel_tol=1e-15)
assert math.isclose(pretrain_flops, cp["pretrain_flops"], rel_tol=1e-15)
assert math.isclose(sft_hours, cp["sft_hours"], rel_tol=1e-15)
s1 = REF["s1"]
s1_flops = 6 * s1["n_params"] * s1["tokens"]
s1_minutes = s1_flops / (s1["accelerators"] * cp["accelerator_flops"] * cp["mfu"]) / 60
assert math.isclose(s1_flops, s1["flops"], rel_tol=1e-15)
assert math.isclose(s1_minutes, s1["minutes"], rel_tol=1e-15)
print('The SFT and s1 compute scenarios were reproduced')
print(f"SFT approximation: {sft_flops:.3e} FLOPs, ×{pretrain_flops/sft_flops:,.0f} below pretraining, {sft_hours:.1f} h")
print(f"s1 scenario: {s1_flops:.3e} FLOPs, {s1_minutes:.1f} min on {s1['accelerators']} accelerators")9. Verifier-gated best-of-N
For independent attempts, p_pass=0.3 gives coverage 1-(1-p_pass)^N. Retained examples per generation fall as N grows. A separate quality scenario uses correctness 0.3, sensitivity 0.9, and false acceptance 0.1, giving a different pass rate of 0.34. We also enumerate a three-candidate joint law to see why pairwise correlation cannot determine coverage.
best = REF["best_of_n"]
p_pass = best["pass_probability"]
coverage = {int(n): 1 - (1 - p_pass) ** int(n) for n in best["coverage"]}
for n, value in coverage.items():
assert math.isclose(value, best["coverage"][str(n)], abs_tol=1e-15)
ordered = sorted(coverage)
assert all(coverage[a] < coverage[b] for a, b in zip(ordered, ordered[1:]))
assert all(coverage[a] / a > coverage[b] / b for a, b in zip(ordered, ordered[1:]))
print('Coverage increases while yield/N decreases')
for n in ordered:
print(f"N={n:2d}: coverage={coverage[n]:.4f}, yield/N={coverage[n]/n:.4f}")
# A separate scenario: correctness and acceptance are different events.
from fractions import Fraction
from itertools import product
pc, sensitivity, false_accept = Fraction(3,10), Fraction(9,10), Fraction(1,10)
pass_rate = pc*sensitivity + (1-pc)*false_accept
precision = pc*sensitivity/pass_rate
assert pass_rate == Fraction(17,50) and precision == Fraction(27,34)
print("separate verifier scenario:", float(pass_rate), float(precision))
# Enumerate the symmetric law of three success indicators.
mass_by_count = [Fraction(1,10),Fraction(1,2),Fraction(1,5),Fraction(1,5)]
joint = {bits: mass_by_count[sum(bits)] / math.comb(3,sum(bits))
for bits in product((0,1),repeat=3)}
assert sum(joint.values()) == 1
marginal = sum(prob*bits[0] for bits,prob in joint.items())
pair = sum(prob*bits[0]*bits[1] for bits,prob in joint.items())
union = sum(prob for bits,prob in joint.items() if any(bits))
independent = 1-(1-marginal)**3
assert marginal == Fraction(1,2) and pair == Fraction(4,15)
assert pair > marginal**2 and union == Fraction(9,10) > independent
perfectly_shared = marginal # all three indicators are identical
print("coverage: joint / independent / perfectly shared:",
float(union), float(independent), float(perfectly_shared))10. LoRA and QLoRA: strict units
Division by 2**20 and 2**30 means MiB and GiB. The four-bit base below is only
the raw parameter payload. The QLoRA parameter-state floor excludes activations,
temporary dequantization buffers, allocator overhead, and sharding, so it is not total
card occupancy.
def lora_parameter_count(
projections: Iterable[tuple[int, int]],
rank: int,
layers: int,
) -> tuple[int, int]:
"""Return (per_layer, total) for rank*(d_in+d_out) per targeted matrix."""
if not isinstance(rank, int) or rank <= 0 or not isinstance(layers, int) or layers <= 0:
raise ValueError("rank and layers must be positive integers")
per_layer = 0
for d_in, d_out in projections:
if d_in <= 0 or d_out <= 0:
raise ValueError("projection dimensions must be positive")
per_layer += rank * (d_in + d_out)
return per_layer, per_layer * layers
lo = REF["lora"]
d, d_kv, rank, layers = lo["d_model"], lo["d_kv"], lo["rank"], lo["layers"]
per_layer, params = lora_parameter_count(
[(d, d), (d, d_kv), (d, d_kv), (d, d)],
rank=rank,
layers=layers,
)
assert per_layer == lo["per_layer"] and params == lo["params"]
full_gib = 16 * cp["n_params"] / 2**30
states_mib = 16 * params / 2**20
adapter_mib = 2 * params / 2**20
base_payload_gib = 0.5 * cp["n_params"] / 2**30
qlora_floor_gib = base_payload_gib + states_mib / 1024
assert math.isclose(full_gib, lo["full_training_states_gib"], abs_tol=1e-12)
assert math.isclose(states_mib, lo["lora_training_states_mib"], abs_tol=1e-12)
assert math.isclose(adapter_mib, lo["lora_adapter_bf16_mib"], abs_tol=1e-12)
assert math.isclose(qlora_floor_gib, lo["qlora_parameter_floor_gib"], abs_tol=1e-12)
print('LoRA parameters and the GiB/MiB floor estimate were reproduced')
print(f"LoRA: {params/1e6:.2f}M params ({params/cp['n_params']*100:.3f}%)")
print(f"training states: full {full_gib:.1f} GiB → LoRA {states_mib:.0f} MiB (×{full_gib*1024/states_mib:.0f})")
print(f"4-bit payload {base_payload_gib:.2f} GiB + adapter states {states_mib:.0f} MiB (includes {adapter_mib:.0f} MiB weights)")
print(f"QLoRA parameter-state floor: {qlora_floor_gib:.2f} GiB (not total card memory)")11. From prepared data to an actual training run
A tiny causal model learns to reverse a pair of symbols. Before optimization, separate and packed dialogues must agree on logits, summed loss and gradients. We then train, generate on held-out pairs, and round-trip the weights in memory. Each answer has three targets: two symbols and an end marker. Diagonal pairs are held out, testing unseen combinations of familiar symbols rather than language generalization. The code reports accuracy without assuming perfection.
"""CPU teaching experiment: serialize, pack, train, generate, and reload.
Synthetic symbol reversal exposes the data contract; it is not evidence about LLM quality.
"""
import copy
import importlib.util
import io
import subprocess
import sys
if importlib.util.find_spec("torch") is None:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "torch"])
import torch
from torch import nn
from torch.nn import functional as F
torch.set_num_threads(1)
torch.manual_seed(130)
VOCAB, WIDTH, BOS, USER, ASSISTANT, END = 16, 32, 0, 1, 2, 3
def dialogue(a, b):
# Reverse two symbols. Role headers are inputs; answer and END are targets.
ids = torch.tensor([BOS, USER, a, b, END, ASSISTANT, b, a, END])
labels = ids.clone()
labels[:6] = -100
return ids, labels
def pack(records):
ids, labels, positions, segments = [], [], [], []
for i, (x, y) in enumerate(records):
target = y.clone()
target[0] = -100 # No prediction across an independent example boundary.
ids.append(x); labels.append(target)
positions.append(torch.arange(len(x)))
segments.append(torch.full_like(x, i))
return tuple(torch.cat(xs) for xs in (ids, labels, positions, segments))
class TinyCausalLM(nn.Module):
def __init__(self):
super().__init__()
self.token = nn.Embedding(VOCAB, WIDTH)
self.position = nn.Embedding(16, WIDTH)
self.qkv = nn.Linear(WIDTH, 3 * WIDTH)
self.proj = nn.Linear(WIDTH, WIDTH)
self.norm = nn.LayerNorm(WIDTH)
self.ff = nn.Sequential(nn.Linear(WIDTH, 64), nn.GELU(), nn.Linear(64, WIDTH))
self.head = nn.Linear(WIDTH, VOCAB)
def forward(self, ids, positions, segments):
h = self.token(ids) + self.position(positions)
q, k, v = self.qkv(h).chunk(3, dim=-1)
idx = torch.arange(len(ids))
allowed = (segments[:, None] == segments[None, :]) & (idx[:, None] >= idx[None, :])
scores = q @ k.T / WIDTH**.5
h = self.norm(h + self.proj(scores.masked_fill(~allowed, -torch.inf).softmax(-1) @ v))
return self.head(h + self.ff(h))
def loss_sum(model, batch):
x, y, pos, seg = batch
logits = model(x, pos, seg)
return F.cross_entropy(logits[:-1], y[1:], ignore_index=-100, reduction="sum")
train = [dialogue(a, b) for a in range(4, 12) for b in range(4, 12) if a != b]
heldout = [dialogue(a, a) for a in range(4, 12)]
model = TinyCausalLM()
# Packing preserves logits, summed loss and gradients when positions and boundaries match.
records = train[:3]
packed_batch = pack(records)
with torch.no_grad():
packed_logits = model(packed_batch[0], packed_batch[2], packed_batch[3])
individual = torch.cat([model(b[0], b[2], b[3]) for b in map(lambda r: pack([r]), records)])
torch.testing.assert_close(packed_logits, individual, rtol=1e-5, atol=1e-6)
packed_loss = loss_sum(model, packed_batch)
separate_loss = sum(loss_sum(model, pack([r])) for r in records)
torch.testing.assert_close(packed_loss, separate_loss, rtol=1e-6, atol=1e-6)
gp = torch.autograd.grad(packed_loss, tuple(model.parameters()))
gs = torch.autograd.grad(separate_loss, tuple(model.parameters()))
for left, right in zip(gp, gs):
torch.testing.assert_close(left, right, rtol=1e-4, atol=3e-6)
def score(model, records):
with torch.no_grad():
return sum(float(loss_sum(model, pack([r]))) for r in records) / (3 * len(records))
before = {"train": score(model, train), "heldout": score(model, heldout)}
optimizer = torch.optim.AdamW(model.parameters(), lr=.006)
history = []
for step in range(240):
# Eight independent examples per packed row; all 56 rotate through training.
subset = [train[(step * 8 + j) % len(train)] for j in range(8)]
batch = pack(subset)
loss = loss_sum(model, batch) / int((batch[1][1:] != -100).sum())
optimizer.zero_grad(); loss.backward(); optimizer.step()
history.append(float(loss.detach()))
after = {"train": score(model, train), "heldout": score(model, heldout)}
assert after["train"] < before["train"] / 5
@torch.no_grad()
def generate(model, prompt, max_new=4):
sequence = prompt.clone()
output = []
for _ in range(max_new):
logits = model(sequence, torch.arange(len(sequence)), torch.zeros_like(sequence))
token = int(logits[-1].argmax())
output.append(token)
sequence = torch.cat([sequence, torch.tensor([token])])
if token == END:
break
return output
# Generate once per held-out example, then derive every metric from stored outputs.
generated = [generate(model, ids[:6]) for ids, _ in heldout]
exact = sum(pred == ids[6:].tolist() for pred, (ids, _) in zip(generated, heldout)) / len(heldout)
stop_rate = sum(bool(pred) and pred[-1] == END for pred in generated) / len(generated)
buffer = io.BytesIO()
torch.save(model.state_dict(), buffer); buffer.seek(0)
restored = TinyCausalLM()
restored.load_state_dict(torch.load(buffer, weights_only=True))
with torch.no_grad():
probe = pack(heldout[:2])
torch.testing.assert_close(model(probe[0], probe[2], probe[3]), restored(probe[0], probe[2], probe[3]), rtol=0, atol=0)
print("token NLL before / after:", before, after)
print("held-out exact / stop rate:", exact, stop_rate, "stored outputs:", generated)
print("packed logits, summed loss, gradients and checkpoint reload: PASS")PyTorch bridge · B20 — Assistant-only labels
This cell embeds the canonical NumPy→PyTorch bridge and runs its parity check. It is self-contained and does not import repository files.
Use nonnegative np.int64 / torch.long IDs and a boolean mask of the same shape; Torch inputs share a device. The result stores labels at their own positions. Align logits[..., :-1, :] with labels[..., 1:] exactly once before cross-entropy. A model that shifts internally must receive unshifted arrays. The loss function checks that active targets exist and belong to its vocabulary.
import importlib.util
import subprocess
import sys
missing = []
if importlib.util.find_spec("numpy") is None:
missing.append("numpy>=1.26")
if importlib.util.find_spec("torch") is None:
missing.append("torch>=2.5")
if missing:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", *missing])
import numpy as np
import torch
import torch.nn.functional as F
import math
def numpy_assistant_labels(input_ids: np.ndarray, assistant_mask: np.ndarray, *, ignore_index: int = -100) -> np.ndarray:
ids, mask = np.asarray(input_ids), np.asarray(assistant_mask)
if ids.dtype != np.int64 or mask.dtype != np.bool_:
raise TypeError("use int64 token IDs and a boolean assistant mask")
if np.any(ids < 0):
raise ValueError("input token IDs must be nonnegative")
if ids.shape != mask.shape:
raise ValueError("input_ids and assistant_mask must have identical shapes")
return np.where(mask, ids, ignore_index).astype(np.int64)
def torch_assistant_labels(input_ids: torch.Tensor, assistant_mask: torch.Tensor, *, ignore_index: int = -100) -> torch.Tensor:
if input_ids.dtype != torch.long or assistant_mask.dtype != torch.bool:
raise TypeError("use torch.long token IDs and a boolean assistant mask")
if input_ids.device != assistant_mask.device:
raise ValueError("input IDs and mask must share a device")
if torch.any(input_ids < 0):
raise ValueError("input token IDs must be nonnegative")
if input_ids.shape != assistant_mask.shape:
raise ValueError("input_ids and assistant_mask must have identical shapes")
labels = input_ids.clone()
return labels.masked_fill(~assistant_mask.to(torch.bool), ignore_index)
ids_np = np.array([10,11,12,13,14,15]); mask_np = np.array([0,0,0,1,1,1], dtype=bool)
np_labels = numpy_assistant_labels(ids_np, mask_np); t_labels = torch_assistant_labels(torch.tensor(ids_np), torch.tensor(mask_np))
np.testing.assert_array_equal(np_labels, t_labels.numpy()); assert np_labels.tolist() == [-100,-100,-100,13,14,15]
# Labels are stored at their own positions. Shift them once for explicit CE.
scores = torch.zeros((len(ids_np), 20), dtype=torch.float64, requires_grad=True)
loss = torch.nn.functional.cross_entropy(scores[:-1], t_labels[1:], ignore_index=-100)
torch.testing.assert_close(loss, torch.log(torch.tensor(20.,dtype=torch.float64)))
loss.backward()
assert torch.count_nonzero(scores.grad[-1]) == 0
assert torch.count_nonzero(scores.grad[:2]) == 0
for ids_bad, mask_bad in [(ids_np.astype(float),mask_np),
(ids_np,mask_np.astype(int)),
(np.array([-1,1]),np.array([False,True]))]:
for run in (lambda: numpy_assistant_labels(ids_bad,mask_bad),
lambda: torch_assistant_labels(torch.tensor(ids_bad),torch.tensor(mask_bad))):
try:
run()
except (TypeError,ValueError):
pass
else:
raise AssertionError("invalid label input was accepted")
print("B20 assistant labels: PASS")PyTorch bridge · B21 — Packed-segment isolation
This cell embeds the canonical NumPy→PyTorch bridge and runs its parity check. It is self-contained and does not import repository files.
import importlib.util
import subprocess
import sys
missing = []
if importlib.util.find_spec("numpy") is None:
missing.append("numpy>=1.26")
if importlib.util.find_spec("torch") is None:
missing.append("torch>=2.5")
if missing:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", *missing])
import numpy as np
import torch
import torch.nn.functional as F
import math
def numpy_segment_position_ids(segment_ids: np.ndarray, *, padding_id: int = -1) -> np.ndarray:
"""Reset positions at every contiguous segment boundary; padding receives position zero."""
segments = np.asarray(segment_ids)
if segments.ndim not in (1, 2):
raise ValueError("segment_ids must be [T] or [B,T]")
batch = segments[None, :] if segments.ndim == 1 else segments
positions = np.zeros_like(batch, dtype=np.int64)
for row in range(batch.shape[0]):
for col in range(1, batch.shape[1]):
current, previous = batch[row, col], batch[row, col - 1]
if current != padding_id and current == previous:
positions[row, col] = positions[row, col - 1] + 1
return positions[0] if segments.ndim == 1 else positions
def torch_segment_position_ids(segment_ids: torch.Tensor, *, padding_id: int = -1) -> torch.Tensor:
"""Pure-tensor counterpart of :func:`numpy_segment_position_ids`."""
if segment_ids.ndim not in (1, 2):
raise ValueError("segment_ids must be [T] or [B,T]")
batch = segment_ids.unsqueeze(0) if segment_ids.ndim == 1 else segment_ids
positions = torch.zeros_like(batch, dtype=torch.long)
for col in range(1, batch.shape[-1]):
continues = (batch[:, col] != padding_id) & (batch[:, col] == batch[:, col - 1])
positions[:, col] = torch.where(continues, positions[:, col - 1] + 1, 0)
return positions[0] if segment_ids.ndim == 1 else positions
def numpy_block_causal_mask(segment_ids: np.ndarray, *, padding_id: int = -1) -> np.ndarray:
"""Bool allowed-mask for causal attention inside each contiguous non-padding segment."""
segments = np.asarray(segment_ids)
if segments.ndim not in (1, 2):
raise ValueError("segment_ids must be [T] or [B,T]")
batch = segments[None, :] if segments.ndim == 1 else segments
t = batch.shape[-1]
boundaries = np.ones_like(batch, dtype=bool)
if t > 1:
boundaries[:, 1:] = batch[:, 1:] != batch[:, :-1]
run_ids = np.cumsum(boundaries, axis=-1)
same = run_ids[:, :, None] == run_ids[:, None, :]
valid = (batch[:, :, None] != padding_id) & (batch[:, None, :] != padding_id)
causal = np.arange(t)[None, None, :] <= np.arange(t)[None, :, None]
out = same & valid & causal
return out[0] if segments.ndim == 1 else out
def torch_block_causal_mask(segment_ids: torch.Tensor, *, padding_id: int = -1) -> torch.Tensor:
if segment_ids.ndim not in (1, 2):
raise ValueError("segment_ids must be [T] or [B,T]")
batch = segment_ids.unsqueeze(0) if segment_ids.ndim == 1 else segment_ids
t = batch.shape[-1]
boundaries = torch.ones_like(batch, dtype=torch.bool)
if t > 1:
boundaries[:, 1:] = batch[:, 1:] != batch[:, :-1]
run_ids = boundaries.to(torch.long).cumsum(dim=-1)
same = run_ids[:, :, None] == run_ids[:, None, :]
valid = (batch[:, :, None] != padding_id) & (batch[:, None, :] != padding_id)
idx = torch.arange(t, device=batch.device)
causal = idx[None, None, :] <= idx[None, :, None]
out = same & valid & causal
return out[0] if segment_ids.ndim == 1 else out
segments_np = np.array([0,0,0,1,1,1,-1]); np_pos = numpy_segment_position_ids(segments_np); np_mask = numpy_block_causal_mask(segments_np)
t_segments = torch.tensor(segments_np); t_pos = torch_segment_position_ids(t_segments); t_mask = torch_block_causal_mask(t_segments)
np.testing.assert_array_equal(np_pos, t_pos.numpy()); np.testing.assert_array_equal(np_mask, t_mask.numpy())
rng = np.random.default_rng(21); x = torch.tensor(rng.standard_normal((1,1,7,4)), dtype=torch.float64); mask = t_mask[None,None]
out_a = torch.nn.functional.scaled_dot_product_attention(x, x, x, attn_mask=mask)
x_changed = x.clone(); x_changed[..., :3, :] += 1000
out_b = torch.nn.functional.scaled_dot_product_attention(x_changed, x_changed, x_changed, attn_mask=mask)
torch.testing.assert_close(out_a[..., 3:6, :], out_b[..., 3:6, :], rtol=0, atol=0)
repeated = np.array([0,0,1,1,0,0])
np.testing.assert_array_equal(numpy_segment_position_ids(repeated), [0,1,0,1,0,1])
repeat_mask = numpy_block_causal_mask(repeated)
assert not repeat_mask[4,0] and repeat_mask[5,4]
print("B21 packed isolation: PASS")Exercises
-
[auto] Add an
analysischannel, make it trainable only behind an explicit flag, and verify the active-target count. -
[auto] Replace FFD with best-fit decreasing and compare bin counts over 100 log-normal length sets.
-
[auto] Corrupt one
position_idafter a segment boundary and write an assertion that localizes the error to the originalexample_index. -
[auto] Implement a mode without a materialized block mask: verify isolation from
cu_seq_lensand a random sample of query/key pairs. -
[project] Connect a real tokenizer through
apply_chat_template, compare its labels with this teaching contract, and feed the packed batch to a small causal LM.
The mandatory path is CPU-safe and repository-independent. It is a complete teaching path through SFT tensor preparation, not a production training system.
Module 13 project-exercise demo — From dialogues to training; full updates and LoRA
Project demonstration
Download .ipynb module_13_project_demo.ipynb · 14.3 KB
Show notebook text
Module 13 project-exercise demo — From dialogues to training; full updates and LoRA
Implementation demo, not a completed full-scale project. Demo 1 serializes, packs, compares gradients, trains, generates and reloads a small CPU model. Demo 2 isolates adapter rank with a shared starting matrix. All data is synthetic; no network or keys are required. All inputs are small, deterministic, offline, and CPU-friendly. The goal is to validate interfaces, metrics, invariants, and report structure before spending GPU or API budget.
from __future__ import annotations
import importlib.util, subprocess, sys
for package in ("numpy", "matplotlib"):
if importlib.util.find_spec(package) is None:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", package])
import math
import random
import time
from collections import Counter, defaultdict
import matplotlib.pyplot as plt
import numpy as np
SEED = 20260801
random.seed(SEED)
np.random.seed(SEED)
print(f"reproducible smoke tier; seed={SEED}")
DEMO_LANGUAGE = 'en'
Demo 1. From dialogue labels to learning
The first experiment follows labels all the way into learning. Each answer has three targets: the reversed symbol pair and END. Before training, separate and packed examples must agree on logits, summed loss, and gradients. Fifty-six unequal pairs train the network from scratch; eight equal pairs are held out. This tests the data contract and transfer to unseen combinations, rather than adaptation of a pretrained language model.
"""CPU teaching experiment: serialize, pack, train, generate, and reload.
Synthetic symbol reversal exposes the data contract; it is not evidence about LLM quality.
"""
import copy
import importlib.util
import io
import subprocess
import sys
if importlib.util.find_spec("torch") is None:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "torch"])
import torch
from torch import nn
from torch.nn import functional as F
torch.set_num_threads(1)
torch.manual_seed(130)
VOCAB, WIDTH, BOS, USER, ASSISTANT, END = 16, 32, 0, 1, 2, 3
def dialogue(a, b):
# Reverse two symbols. Role headers are inputs; answer and END are targets.
ids = torch.tensor([BOS, USER, a, b, END, ASSISTANT, b, a, END])
labels = ids.clone()
labels[:6] = -100
return ids, labels
def pack(records):
ids, labels, positions, segments = [], [], [], []
for i, (x, y) in enumerate(records):
target = y.clone()
target[0] = -100 # No prediction across an independent example boundary.
ids.append(x); labels.append(target)
positions.append(torch.arange(len(x)))
segments.append(torch.full_like(x, i))
return tuple(torch.cat(xs) for xs in (ids, labels, positions, segments))
class TinyCausalLM(nn.Module):
def __init__(self):
super().__init__()
self.token = nn.Embedding(VOCAB, WIDTH)
self.position = nn.Embedding(16, WIDTH)
self.qkv = nn.Linear(WIDTH, 3 * WIDTH)
self.proj = nn.Linear(WIDTH, WIDTH)
self.norm = nn.LayerNorm(WIDTH)
self.ff = nn.Sequential(nn.Linear(WIDTH, 64), nn.GELU(), nn.Linear(64, WIDTH))
self.head = nn.Linear(WIDTH, VOCAB)
def forward(self, ids, positions, segments):
h = self.token(ids) + self.position(positions)
q, k, v = self.qkv(h).chunk(3, dim=-1)
idx = torch.arange(len(ids))
allowed = (segments[:, None] == segments[None, :]) & (idx[:, None] >= idx[None, :])
scores = q @ k.T / WIDTH**.5
h = self.norm(h + self.proj(scores.masked_fill(~allowed, -torch.inf).softmax(-1) @ v))
return self.head(h + self.ff(h))
def loss_sum(model, batch):
x, y, pos, seg = batch
logits = model(x, pos, seg)
return F.cross_entropy(logits[:-1], y[1:], ignore_index=-100, reduction="sum")
train = [dialogue(a, b) for a in range(4, 12) for b in range(4, 12) if a != b]
heldout = [dialogue(a, a) for a in range(4, 12)]
model = TinyCausalLM()
# Packing preserves logits, summed loss and gradients when positions and boundaries match.
records = train[:3]
packed_batch = pack(records)
with torch.no_grad():
packed_logits = model(packed_batch[0], packed_batch[2], packed_batch[3])
individual = torch.cat([model(b[0], b[2], b[3]) for b in map(lambda r: pack([r]), records)])
torch.testing.assert_close(packed_logits, individual, rtol=1e-5, atol=1e-6)
packed_loss = loss_sum(model, packed_batch)
separate_loss = sum(loss_sum(model, pack([r])) for r in records)
torch.testing.assert_close(packed_loss, separate_loss, rtol=1e-6, atol=1e-6)
gp = torch.autograd.grad(packed_loss, tuple(model.parameters()))
gs = torch.autograd.grad(separate_loss, tuple(model.parameters()))
for left, right in zip(gp, gs):
torch.testing.assert_close(left, right, rtol=1e-4, atol=3e-6)
def score(model, records):
with torch.no_grad():
return sum(float(loss_sum(model, pack([r]))) for r in records) / (3 * len(records))
before = {"train": score(model, train), "heldout": score(model, heldout)}
optimizer = torch.optim.AdamW(model.parameters(), lr=.006)
history = []
for step in range(240):
# Eight independent examples per packed row; all 56 rotate through training.
subset = [train[(step * 8 + j) % len(train)] for j in range(8)]
batch = pack(subset)
loss = loss_sum(model, batch) / int((batch[1][1:] != -100).sum())
optimizer.zero_grad(); loss.backward(); optimizer.step()
history.append(float(loss.detach()))
after = {"train": score(model, train), "heldout": score(model, heldout)}
assert after["train"] < before["train"] / 5
@torch.no_grad()
def generate(model, prompt, max_new=4):
sequence = prompt.clone()
output = []
for _ in range(max_new):
logits = model(sequence, torch.arange(len(sequence)), torch.zeros_like(sequence))
token = int(logits[-1].argmax())
output.append(token)
sequence = torch.cat([sequence, torch.tensor([token])])
if token == END:
break
return output
# Generate once per held-out example, then derive every metric from stored outputs.
generated = [generate(model, ids[:6]) for ids, _ in heldout]
exact = sum(pred == ids[6:].tolist() for pred, (ids, _) in zip(generated, heldout)) / len(heldout)
stop_rate = sum(bool(pred) and pred[-1] == END for pred in generated) / len(generated)
buffer = io.BytesIO()
torch.save(model.state_dict(), buffer); buffer.seek(0)
restored = TinyCausalLM()
restored.load_state_dict(torch.load(buffer, weights_only=True))
with torch.no_grad():
probe = pack(heldout[:2])
torch.testing.assert_close(model(probe[0], probe[2], probe[3]), restored(probe[0], probe[2], probe[3]), rtol=0, atol=0)
print("token NLL before / after:", before, after)
print("held-out exact / stop rate:", exact, stop_rate, "stored outputs:", generated)
print("packed logits, summed loss, gradients and checkpoint reload: PASS")Agreement before training checks more than attention geometry: it also tests target alignment and gradient weighting. Afterward, distinguish answer exact match from stopping rate; a well-terminated answer may still be wrong. Every generation metric uses the same stored outputs. Reloading weights preserves predictions on a common probe, but none of these checks establishes natural-language generalization.
Demo 2. Adapter rank and held-out error
The second task has a known rank-four change to one matrix. Full updates and adapters use the same starting matrix and data. The code multiplies row vectors, so its factor order and shapes are transposed relative to the lecture’s column-vector notation. Set alpha/r to one. Randomly initializing one factor and zeroing the other preserves the initial prediction while leaving a route for learning.
# Same base and data for full updates and LoRA; the target change has rank four.
torch.manual_seed(131)
x_train, x_test = torch.randn(256, 32), torch.randn(128, 32)
base = torch.randn(32, 32) / 32**.5
delta = torch.randn(32, 4) @ torch.randn(4, 32) / 8
y_train, y_test = x_train @ (base + delta), x_test @ (base + delta)
results = []
for rank in (None, 2, 4, 8):
torch.manual_seed(132)
frozen = base.clone()
if rank is None:
update = torch.zeros_like(base, requires_grad=True)
params = [update]
predict = lambda x: x @ (frozen + update)
else:
a = (torch.randn(32, rank) * .03).requires_grad_()
b = torch.zeros(rank, 32, requires_grad=True)
params = [a, b]
predict = lambda x: x @ frozen + (x @ a) @ b # alpha/r = 1
optimizer = torch.optim.Adam(params, lr=.05)
for _ in range(240):
loss = (predict(x_train) - y_train).square().mean()
optimizer.zero_grad(); loss.backward(); optimizer.step()
with torch.no_grad():
train_mse = float((predict(x_train)-y_train).square().mean())
test_predictions = predict(x_test)
test_mse = float((test_predictions-y_test).square().mean())
assert torch.equal(frozen, base)
assert test_predictions.shape == y_test.shape
assert torch.isfinite(test_predictions).all()
assert torch.isfinite(torch.tensor([train_mse, test_mse])).all()
results.append(dict(rank=rank, trainable=sum(p.numel() for p in params),
train_mse=train_mse, test_mse=test_mse))
for result in results: print(result)
comparison = results[2]['test_mse'] < results[1]['test_mse']
label = ('Ошибка ранга 4 меньше ошибки ранга 2 в этом запуске:' if DEMO_LANGUAGE == 'ru'
else 'Rank 4 has lower held-out error than rank 2 in this run:')
print(label, comparison)
print('This is a matrix adaptation experiment, not a measured GPU-memory or LLM-quality comparison.')The full update has 1,024 parameters; ranks 2, 4, and 8 use 128, 256, and 512. Rank two cannot exactly express the specified rank-four change. The larger adapters can represent it, although a finite optimization run need not reach the exact solution. Read held-out MSE as the result of this run. Vary the training-set size and the true update rank, then compare the errors. With few examples, the higher-rank adapter may generalize less well. That is a valid observation; the checks enforce shapes, finite computations, and an unchanged base matrix. The example measures neither runtime nor memory, and its deliberately low-rank task cannot establish that adapters always match full fine-tuning.
Scaling to the full project
For the full project, pin the official tokenizer and template, split task families before synthesis, and preserve token weighting during gradient accumulation. Compare full tuning and LoRA from the same base using held-out tasks and measured runtime and memory.
Minimum report
-
Data, model, and code revisions, plus the random seed.
-
The primary metric, plus quality, cost, latency, and memory where applicable.
-
A baseline, a matched budget, and at least one negative control.
-
Failed runs and the limits of the supported conclusions.