Files
llm-atlas/experiments/k3/attnres_spike/train.py
T

1099 lines
40 KiB
Python

#!/usr/bin/env python3
"""Replay Round 05 Block training with preregistered spike-path diagnostics."""
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
import math
import os
import platform
import statistics
import sys
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable
import torch
import torch.nn as nn
import torch.nn.functional as F
PROTOCOL_ID = "llm-atlas-k3-attnres-spike-path-v1"
PARENT_PROTOCOL_ID = "llm-atlas-k3-attnres-gradient-scale-v1"
DEPTH = 32
SEEDS = (2026073001, 2026073002, 2026073003)
FORMAL_STEPS = 8000
DIAGNOSTIC_STEPS = (0, 100, 500, 2000, 4000, 8000)
INTERVENTION_STEPS = (0, 8000)
INTERVENTION_MODES = (
"learned",
"detached_learned",
"uniform_value_backward",
)
POSITIONS = (
"pre_attention_input",
"attention_branch_output",
"post_attention_state",
"pre_mlp_input",
"mlp_branch_output",
"post_mlp_state",
)
REDUCTIONS = (
"element_rms",
"token_rms_mean",
"token_rms_median",
"token_rms_p95",
"batch_mean_rms",
"token_mean_rms",
"global_l2",
)
SPIKE_LAYERS = (21, 22, 23, 24, 25)
CONTEXT = 256
VOCABULARY = 256
DIAGNOSTIC_WINDOWS = 16
VALIDATION_WINDOWS = 64
EVAL_BATCH_SIZE = 8
TRAIN_BATCH_SIZE = 32
TIMING_WARMUP = 20
EPSILON = 1e-30
SPECTRUM_TOLERANCE = 1e-6
SCALE_TOLERANCE = 1e-5
def load_parent() -> Any:
path = Path(__file__).resolve().parents[1] / "attnres_gradient" / "train.py"
spec = importlib.util.spec_from_file_location(
"k3_attnres_gradient_parent", path
)
if spec is None or spec.loader is None:
raise RuntimeError(f"cannot import parent runner from {path}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
parent = load_parent()
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--run-kind", choices=("smoke", "formal", "replay"), required=True)
parser.add_argument("--seed", type=int, required=True)
parser.add_argument("--cache-dir", type=Path, required=True)
parser.add_argument("--parent-manifest", type=Path, required=True)
parser.add_argument("--manifest", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
if args.seed not in SEEDS:
raise ValueError(f"seed is not preregistered: {args.seed}")
if args.run_kind == "replay" and args.seed != SEEDS[0]:
raise ValueError("the preregistered replay is seed 2026073001")
return args
def file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def canonical_sha256(value: Any) -> str:
payload = json.dumps(
value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
).encode()
return hashlib.sha256(payload).hexdigest()
def tensors_sha256(values: Iterable[torch.Tensor]) -> str:
digest = hashlib.sha256()
for index, value in enumerate(values):
digest.update(f"{index}\0".encode())
digest.update(parent.tensor_bytes(value))
return digest.hexdigest()
def type7_quantile(sorted_values: torch.Tensor, probability: float) -> float:
if sorted_values.ndim != 1 or sorted_values.numel() == 0:
raise ValueError("Type-7 quantile requires a non-empty flat tensor")
h = (sorted_values.numel() - 1) * probability
lower = math.floor(h)
fraction = h - lower
if fraction == 0:
value = sorted_values[lower]
else:
value = sorted_values[lower] + fraction * (
sorted_values[lower + 1] - sorted_values[lower]
)
return value.detach().cpu().item()
def mean(values: Iterable[float]) -> float:
return statistics.fmean(values)
def layer_statistics(values: list[float]) -> dict[str, Any]:
if len(values) != DEPTH or any(not math.isfinite(value) or value <= 0 for value in values):
raise RuntimeError("layer metric must contain 32 finite positive values")
base = parent.depth_statistics(values)
spike = [values[layer - 1] for layer in SPIKE_LAYERS]
rest = [
value
for layer, value in enumerate(values, start=1)
if layer not in SPIKE_LAYERS
]
ordered = sorted(
range(1, DEPTH + 1), key=lambda layer: (-values[layer - 1], layer)
)
base.update(
{
"spike_layers": list(SPIKE_LAYERS),
"spike_mean": mean(spike),
"rest_mean": mean(rest),
"spike_contrast": mean(spike) / mean(rest),
"peak_layer": ordered[0],
"peak_normalized": values[ordered[0] - 1] / base["mean"],
"top_five_layers": ordered[:5],
"top_five_spike_overlap": len(set(ordered[:5]) & set(SPIKE_LAYERS)),
}
)
return base
def gradient_reductions(gradient: torch.Tensor) -> dict[str, float]:
value = gradient.detach().float()
if value.shape != (DIAGNOSTIC_WINDOWS, CONTEXT, parent.round04.D_MODEL):
raise RuntimeError(f"unexpected gradient shape: {tuple(value.shape)}")
if not torch.isfinite(value).all():
raise RuntimeError("non-finite activation gradient")
token_rms = value.square().mean(dim=-1).sqrt().reshape(-1)
token_rms_sorted = torch.sort(token_rms).values
result = {
"element_rms": value.square().mean().sqrt().detach().cpu().item(),
"token_rms_mean": token_rms.mean().detach().cpu().item(),
"token_rms_median": type7_quantile(token_rms_sorted, 0.5),
"token_rms_p95": type7_quantile(token_rms_sorted, 0.95),
"batch_mean_rms": value.mean(dim=0).square().mean().sqrt().detach().cpu().item(),
"token_mean_rms": value.mean(dim=1).square().mean().sqrt().detach().cpu().item(),
"global_l2": value.square().sum().sqrt().detach().cpu().item(),
}
if any(not math.isfinite(metric) or metric <= 0 for metric in result.values()):
raise RuntimeError("gradient reduction is non-finite or non-positive")
expected_l2 = result["element_rms"] * math.sqrt(value.numel())
relative_error = abs(result["global_l2"] - expected_l2) / expected_l2
if relative_error > 1e-6:
raise RuntimeError(f"global L2 algebraic control failed: {relative_error}")
return result
def rms(value: torch.Tensor) -> float:
return value.detach().float().square().mean().sqrt().cpu().item()
def weight_summary(
weights: torch.Tensor,
labels: list[str],
*,
mixer_index: int | None,
layer: int | None,
branch: str,
group: int | None,
offset: int | None,
) -> dict[str, Any]:
detached = weights.detach().float()
if detached.ndim != 3 or detached.shape[0] != len(labels):
raise RuntimeError("mixer weight shape/label mismatch")
source_summaries = []
for label, source in zip(labels, detached):
flattened = torch.sort(source.reshape(-1)).values
source_summaries.append(
{
"label": label,
"mean": flattened.mean().cpu().item(),
"p05": type7_quantile(flattened, 0.05),
"median": type7_quantile(flattened, 0.5),
"p95": type7_quantile(flattened, 0.95),
}
)
entropy = -(
detached * torch.log(detached.clamp_min(1e-30))
).sum(dim=0).mean().cpu().item()
source_count = len(labels)
means = [item["mean"] for item in source_summaries]
return {
"mixer_index": mixer_index,
"layer": layer,
"branch": branch,
"group": group,
"offset": offset,
"sources": source_count,
"source_summaries": source_summaries,
"entropy_mean": entropy,
"normalized_entropy": 1.0 if source_count == 1 else entropy / math.log(source_count),
"max_source_mass": max(means),
"latest_source_mass": means[-1],
}
def recompute_weights(
mixer: nn.Module, sources: list[torch.Tensor]
) -> torch.Tensor:
with torch.no_grad():
values = torch.stack(sources, dim=0)
keys = mixer.key_norm(values)
logits = torch.einsum("d,nbtd->nbt", mixer.query, keys.float())
return torch.softmax(logits, dim=0)
class RoutedSourceBackward(torch.autograd.Function):
@staticmethod
def forward(
ctx: Any,
values: torch.Tensor,
parent_output: torch.Tensor,
backward_weights: torch.Tensor,
) -> torch.Tensor:
ctx.save_for_backward(values, backward_weights)
return parent_output
@staticmethod
def backward(
ctx: Any, grad_output: torch.Tensor
) -> tuple[torch.Tensor, None, None]:
values, backward_weights = ctx.saved_tensors
with torch.enable_grad():
surrogate_values = values.detach().requires_grad_(True)
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
surrogate_output = torch.einsum(
"nbt,nbtd->btd",
backward_weights.detach(),
surrogate_values.float(),
).to(surrogate_values.dtype)
(grad_values,) = torch.autograd.grad(
surrogate_output,
surrogate_values,
grad_output,
retain_graph=False,
create_graph=False,
)
return grad_values, None, None
@dataclass
class SpikeTrace:
positions: dict[str, list[torch.Tensor]]
mixers: list[dict[str, Any]]
class SpikeLanguageModel(parent.GradientLanguageModel):
"""Parent model with a diagnostic-only expanded capture graph."""
def forward(
self,
input_ids: torch.Tensor,
capture: bool = False,
mixer_backward_mode: str = "learned",
) -> tuple[torch.Tensor, SpikeTrace | None]:
if not capture:
if mixer_backward_mode != "learned":
raise RuntimeError("training/evaluation cannot use an intervention")
return super().forward(input_ids, capture=False)
if mixer_backward_mode not in INTERVENTION_MODES:
raise ValueError(f"unknown intervention mode: {mixer_backward_mode}")
return self._diagnostic_forward(input_ids, mixer_backward_mode)
@staticmethod
def _capture(trace: SpikeTrace, position: str, value: torch.Tensor) -> None:
if not value.requires_grad:
raise RuntimeError(f"{position} does not require grad")
value.retain_grad()
trace.positions[position].append(value)
@staticmethod
def _source_labels(
completed_labels: list[str], has_partial: bool
) -> list[str]:
return completed_labels + (["current_group_partial"] if has_partial else [])
def _mix(
self,
mixer: nn.Module,
sources: list[torch.Tensor],
labels: list[str],
*,
mode: str,
mixer_index: int | None,
layer: int | None,
branch: str,
group: int | None,
offset: int | None,
) -> tuple[torch.Tensor, dict[str, Any]]:
parent_output, _ = mixer(sources, False)
weights = recompute_weights(mixer, sources)
summary = weight_summary(
weights,
labels,
mixer_index=mixer_index,
layer=layer,
branch=branch,
group=group,
offset=offset,
)
if mode == "learned":
return parent_output, summary
values = torch.stack(sources, dim=0)
if mode == "detached_learned":
backward_weights = weights
else:
backward_weights = torch.full_like(weights, 1.0 / len(sources))
routed = RoutedSourceBackward.apply(
values, parent_output, backward_weights
)
return routed, summary
def _diagnostic_forward(
self, input_ids: torch.Tensor, mode: str
) -> tuple[torch.Tensor, SpikeTrace]:
trace = SpikeTrace({position: [] for position in POSITIONS}, [])
embedded = self.embed(input_ids)
completed = [embedded]
completed_labels = ["embedding"]
partial: torch.Tensor | None = None
mixer_index = 0
for layer_index, block in enumerate(self.blocks):
layer = layer_index + 1
group = layer_index // 4 + 1
offset = layer_index % 4 + 1
attention_sources = completed + (
[] if partial is None else [partial]
)
attention_labels = self._source_labels(
completed_labels, partial is not None
)
attention_input, summary = self._mix(
self.mixers[mixer_index],
attention_sources,
attention_labels,
mode=mode,
mixer_index=mixer_index,
layer=layer,
branch="attention",
group=group,
offset=offset,
)
trace.mixers.append(summary)
mixer_index += 1
self._capture(trace, "pre_attention_input", attention_input)
attention_output = block.attention(
block.attention_norm(attention_input)
)
self._capture(trace, "attention_branch_output", attention_output)
attention_for_residual = attention_output.float()
partial = (
attention_for_residual
if partial is None
else partial + attention_for_residual
)
self._capture(trace, "post_attention_state", partial)
mlp_sources = completed + [partial]
mlp_labels = self._source_labels(completed_labels, True)
mlp_input, summary = self._mix(
self.mixers[mixer_index],
mlp_sources,
mlp_labels,
mode=mode,
mixer_index=mixer_index,
layer=layer,
branch="mlp",
group=group,
offset=offset,
)
trace.mixers.append(summary)
mixer_index += 1
self._capture(trace, "pre_mlp_input", mlp_input)
mlp_output = block.mlp(block.mlp_norm(mlp_input))
self._capture(trace, "mlp_branch_output", mlp_output)
partial = partial + mlp_output.float()
self._capture(trace, "post_mlp_state", partial)
if mixer_index % parent.round04.SUBLAYERS_PER_BLOCK == 0:
completed.append(partial)
completed_labels.append(f"completed_group_{group}")
partial = None
if partial is not None or len(completed) != 9 or mixer_index != 64:
raise RuntimeError("Block aggregation topology mismatch")
if self.output_mixer is None:
raise RuntimeError("output mixer missing")
hidden, summary = self._mix(
self.output_mixer,
completed,
completed_labels,
mode=mode,
mixer_index=None,
layer=None,
branch="output",
group=None,
offset=None,
)
trace.mixers.append(summary)
normalized = self.final_norm(hidden)
logits = F.linear(normalized, self.token_embedding.weight)
return logits, trace
def validate_trace(trace: SpikeTrace) -> dict[str, Any]:
expected_shape = (DIAGNOSTIC_WINDOWS, CONTEXT, parent.round04.D_MODEL)
capture = {}
for position in POSITIONS:
values = trace.positions[position]
if len(values) != DEPTH:
raise RuntimeError(f"{position} capture count mismatch")
if any(tuple(value.shape) != expected_shape for value in values):
raise RuntimeError(f"{position} capture shape mismatch")
pointers = [value.untyped_storage().data_ptr() for value in values]
if len(set(pointers)) != len(pointers):
raise RuntimeError(f"{position} contains aliased storage")
capture[position] = {
"count": len(values),
"shape": list(expected_shape),
"dtypes": [str(value.dtype) for value in values],
"storage_unique": True,
"activation_sha256": tensors_sha256(values),
"output_rms_by_layer": [rms(value) for value in values],
}
if len(trace.mixers) != 65:
raise RuntimeError("mixer capture count mismatch")
return capture
def run_diagnostic(
model: SpikeLanguageModel,
corpus: Any,
optimizer: torch.optim.Optimizer,
*,
mode: str,
loss_scale: float = 1.0,
) -> dict[str, Any]:
optimizer_before = parent.recursive_state_hash(optimizer.state_dict())
model.eval()
model.zero_grad(set_to_none=True)
inputs, targets = corpus.fixed_batch(
corpus.diagnostic_starts, 0, DIAGNOSTIC_WINDOWS
)
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
logits, trace = model(
inputs, capture=True, mixer_backward_mode=mode
)
unscaled_loss = parent.cross_entropy(logits, targets)
loss = unscaled_loss * loss_scale
if trace is None:
raise RuntimeError("diagnostic trace missing")
capture = validate_trace(trace)
forward = {
"logits_sha256": tensors_sha256([logits]),
"activation_sha256": {
position: capture[position]["activation_sha256"]
for position in POSITIONS
},
"mixer_summary_sha256": canonical_sha256(trace.mixers),
"loss_nats": unscaled_loss.detach().cpu().item(),
}
loss.backward()
positions: dict[str, Any] = {}
for position in POSITIONS:
reductions_by_layer = {name: [] for name in REDUCTIONS}
for value in trace.positions[position]:
if value.grad is None:
raise RuntimeError(f"{position} gradient is missing")
reductions = gradient_reductions(value.grad)
for name in REDUCTIONS:
reductions_by_layer[name].append(reductions[name])
positions[position] = {
"capture": capture[position],
"reductions": {
name: {
"values": values,
"statistics": layer_statistics(values),
}
for name, values in reductions_by_layer.items()
},
"all_gradients_present": True,
"all_gradients_finite": True,
}
model.zero_grad(set_to_none=True)
optimizer_after = parent.recursive_state_hash(optimizer.state_dict())
if optimizer_before != optimizer_after:
raise RuntimeError("diagnostic mutated optimizer state")
return {
"mode": mode,
"loss_scale": loss_scale,
"loss_nats": forward["loss_nats"],
"bits_per_byte": forward["loss_nats"] / math.log(2),
"forward": forward,
"positions": positions,
"mixers": trace.mixers,
"optimizer_state_before": optimizer_before,
"optimizer_state_after": optimizer_after,
"optimizer_state_unchanged": True,
}
def forward_identity_gate(modes: dict[str, Any]) -> dict[str, Any]:
learned = modes["learned"]["forward"]
comparisons = {}
for name in INTERVENTION_MODES[1:]:
other = modes[name]["forward"]
comparisons[name] = {
"logits_exact": other["logits_sha256"] == learned["logits_sha256"],
"loss_exact": other["loss_nats"] == learned["loss_nats"],
"activations_exact": (
other["activation_sha256"] == learned["activation_sha256"]
),
"mixer_summaries_exact": (
other["mixer_summary_sha256"]
== learned["mixer_summary_sha256"]
),
}
passed = all(all(values.values()) for values in comparisons.values())
if not passed:
raise RuntimeError(f"forward identity gate failed: {comparisons}")
return {"passed": True, "comparisons": comparisons}
def initialization_negative_control(modes: dict[str, Any]) -> dict[str, Any]:
learned = modes["learned"]
comparisons = {}
passed = True
for mode in INTERVENTION_MODES[1:]:
per_position = {}
for position in POSITIONS:
left = learned["positions"][position]["reductions"]["element_rms"]
right = modes[mode]["positions"][position]["reductions"]["element_rms"]
raw_errors = [
abs(a - b) / a
for a, b in zip(left["values"], right["values"])
]
normalized_errors = [
abs(a - b)
for a, b in zip(
left["statistics"]["normalized"],
right["statistics"]["normalized"],
)
]
position_passed = (
all(value > 0 and math.isfinite(value) for value in left["values"])
and max(raw_errors) <= SPECTRUM_TOLERANCE
and max(normalized_errors) <= SPECTRUM_TOLERANCE
)
passed = passed and position_passed
per_position[position] = {
"passed": position_passed,
"max_raw_relative_error": max(raw_errors),
"max_normalized_absolute_error": max(normalized_errors),
}
comparisons[mode] = per_position
if not passed:
raise RuntimeError(
"initialization negative control failed: "
+ json.dumps(comparisons, sort_keys=True)
)
return {"passed": True, "comparisons": comparisons}
def loss_scale_gate(base: dict[str, Any], doubled: dict[str, Any]) -> dict[str, Any]:
if base["forward"] != doubled["forward"]:
raise RuntimeError("loss scaling unexpectedly changed the forward pass")
checks = {}
passed = True
for position in POSITIONS:
checks[position] = {}
for reduction in REDUCTIONS:
left = base["positions"][position]["reductions"][reduction]
right = doubled["positions"][position]["reductions"][reduction]
scale_errors = [
abs((b / a) - 2.0)
for a, b in zip(left["values"], right["values"])
]
normalized_errors = [
abs(a - b)
for a, b in zip(
left["statistics"]["normalized"],
right["statistics"]["normalized"],
)
]
cv_error = abs(
left["statistics"]["population_cv"]
- right["statistics"]["population_cv"]
)
contrast_error = abs(
left["statistics"]["spike_contrast"]
- right["statistics"]["spike_contrast"]
)
item_passed = (
max(scale_errors) <= SCALE_TOLERANCE
and max(normalized_errors) <= SPECTRUM_TOLERANCE
and cv_error <= SPECTRUM_TOLERANCE
and contrast_error <= SPECTRUM_TOLERANCE
)
passed = passed and item_passed
checks[position][reduction] = {
"passed": item_passed,
"max_scale_ratio_error": max(scale_errors),
"max_normalized_absolute_error": max(normalized_errors),
"cv_absolute_error": cv_error,
"spike_contrast_absolute_error": contrast_error,
}
if not passed:
raise RuntimeError("loss-scale gate failed")
return {"passed": True, "checks": checks}
def run_diagnostic_bundle(
model: SpikeLanguageModel,
corpus: Any,
optimizer: torch.optim.Optimizer,
step: int,
) -> dict[str, Any]:
learned = run_diagnostic(
model, corpus, optimizer, mode="learned"
)
modes = {"learned": learned}
if step in INTERVENTION_STEPS:
for mode in INTERVENTION_MODES[1:]:
modes[mode] = run_diagnostic(
model, corpus, optimizer, mode=mode
)
result = {"step": step, "modes": modes}
if step in INTERVENTION_STEPS:
result["forward_identity_gate"] = forward_identity_gate(modes)
if step == 0:
result["initialization_negative_control"] = (
initialization_negative_control(modes)
)
doubled = run_diagnostic(
model, corpus, optimizer, mode="learned", loss_scale=2.0
)
result["loss_scale_gate"] = loss_scale_gate(learned, doubled)
model.zero_grad(set_to_none=True)
return result
def frozen_training_compare(
result: dict[str, Any],
parent_raw: dict[str, Any],
) -> dict[str, Any]:
checks = {
"final_model_state": (
result["hashes"]["final_model_state"]
== parent_raw["hashes"]["final_model_state"]
),
"final_optimizer_state": (
result["hashes"]["final_optimizer_state"]
== parent_raw["hashes"]["final_optimizer_state"]
),
"evaluations": result["evaluations"] == parent_raw["evaluations"],
"training_history": (
result["training_history"] == parent_raw["training_history"]
),
}
post_mlp_exact = []
for new, old in zip(result["diagnostics"], parent_raw["diagnostics"]):
new_values = new["modes"]["learned"]["positions"]["post_mlp_state"][
"reductions"
]["element_rms"]["values"]
post_mlp_exact.append(
new["step"] == old["step"]
and new_values == old["activation_grad_rms_by_block"]
)
checks["post_mlp_element_rms_all_steps"] = all(post_mlp_exact)
passed = all(checks.values())
if not passed:
raise RuntimeError(f"Round 05 training equivalence failed: {checks}")
return {"passed": True, "checks": checks}
def load_and_verify_inputs(
args: argparse.Namespace,
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], Path]:
manifest = json.loads(args.manifest.read_text())
parent_manifest = json.loads(args.parent_manifest.read_text())
repo_root = Path(__file__).resolve().parents[3]
if manifest["protocol_id"] != PROTOCOL_ID:
raise RuntimeError("Round 06 manifest protocol mismatch")
if parent_manifest["protocol_id"] != PARENT_PROTOCOL_ID:
raise RuntimeError("parent manifest protocol mismatch")
frozen_fields = {
"architecture": "block",
"depth": DEPTH,
"formal_seeds": list(SEEDS),
"positions": list(POSITIONS),
}
for key, expected_value in frozen_fields.items():
if manifest[key] != expected_value:
raise RuntimeError(f"Round 06 manifest field mismatch: {key}")
if manifest["training"]["steps"] != FORMAL_STEPS:
raise RuntimeError("Round 06 formal-step mismatch")
if manifest["training"]["diagnostic_steps"] != list(DIAGNOSTIC_STEPS):
raise RuntimeError("Round 06 diagnostic-step mismatch")
if manifest["interventions"]["modes"] != list(INTERVENTION_MODES):
raise RuntimeError("Round 06 intervention-mode mismatch")
if manifest["interventions"]["steps"] != list(INTERVENTION_STEPS):
raise RuntimeError("Round 06 intervention-step mismatch")
manifest_reductions = (
manifest["reductions"]["confirmatory"]
+ manifest["reductions"]["exploratory"]
+ manifest["reductions"]["algebraic_control"]
)
if manifest_reductions != list(REDUCTIONS):
raise RuntimeError("Round 06 reduction list mismatch")
expected_parent_hash = manifest["parent_artifacts"]["manifest_sha256"]
if file_sha256(args.parent_manifest) != expected_parent_hash:
raise RuntimeError("parent manifest file hash mismatch")
if file_sha256(Path(parent.__file__)) != manifest["parent_artifacts"]["runner_sha256"]:
raise RuntimeError("parent runner file hash mismatch")
parent_protocol_path = (
repo_root / "research" / "K3_ATTNRES_GRADIENT_SCALE_PROTOCOL.md"
)
if (
file_sha256(parent_protocol_path)
!= manifest["parent_artifacts"]["protocol_sha256"]
):
raise RuntimeError("parent protocol file hash mismatch")
for key in (
"formal_schedule_sha256",
"validation_tensor_sha256",
"diagnostic_tensor_sha256",
):
if parent_manifest["windows"][key] != manifest["parent_artifacts"][key]:
raise RuntimeError(f"parent data hash mismatch: {key}")
parent_raw_path = (
repo_root
/ "experiments"
/ "k3"
/ "attnres_gradient"
/ "results"
/ "raw"
/ f"formal-depth-32-block-seed-{args.seed}.json"
)
expected = manifest["round05_expected"][str(args.seed)]
if file_sha256(parent_raw_path) != expected["raw_file_sha256"]:
raise RuntimeError("Round 05 raw physical hash mismatch")
parent_raw = json.loads(parent_raw_path.read_text())
if parent_raw["canonical_sha256_without_self"] != expected["canonical_sha256"]:
raise RuntimeError("Round 05 raw canonical hash mismatch")
if parent_raw["hashes"]["final_model_state"] != expected["final_model_state"]:
raise RuntimeError("Round 05 expected model-state hash mismatch")
if (
parent_raw["hashes"]["final_optimizer_state"]
!= expected["final_optimizer_state"]
):
raise RuntimeError("Round 05 expected optimizer-state hash mismatch")
return manifest, parent_manifest, parent_raw, repo_root
def main() -> None:
args = parse_args()
if not torch.cuda.is_available():
raise RuntimeError("CUDA is required")
if os.environ.get("CUBLAS_WORKSPACE_CONFIG") != ":4096:8":
raise RuntimeError("CUBLAS_WORKSPACE_CONFIG must be :4096:8")
manifest, parent_manifest, parent_raw, repo_root = load_and_verify_inputs(args)
parent.configure_round04_globals(DEPTH)
parent.configure_determinism(args.seed)
corpus = parent.round04.ByteCorpus(
args.cache_dir, parent_manifest, torch.device("cuda")
)
model = SpikeLanguageModel("block").to(torch.device("cuda"))
initial_public_hash = parent.named_state_hash(
model, include_mixers=False
)
initial_mixer_hash = parent.named_state_hash(
model, include_mixers=True
)
public_structure_hash, public_tensors, public_elements = (
parent.state_structure_hash(model, include_mixers=False)
)
input_gate_hashes = parent.model_input_gate_hashes(
corpus, parent_manifest, args.seed, TRAIN_BATCH_SIZE
)
decay_parameters: list[nn.Parameter] = []
no_decay_parameters: list[nn.Parameter] = []
for parameter in model.parameters():
(decay_parameters if parameter.ndim >= 2 else no_decay_parameters).append(
parameter
)
optimizer = torch.optim.AdamW(
[
{"params": decay_parameters, "weight_decay": parent.WEIGHT_DECAY},
{"params": no_decay_parameters, "weight_decay": 0.0},
],
lr=parent.PEAK_LR,
betas=parent.BETAS,
eps=parent.ADAM_EPS,
)
evaluations = [
{
"step": 0,
**parent.evaluate(
model, corpus, VALIDATION_WINDOWS, EVAL_BATCH_SIZE
),
}
]
diagnostics = [run_diagnostic_bundle(model, corpus, optimizer, 0)]
print(
json.dumps(
{
"event": "diagnostic",
"step": 0,
"seed": args.seed,
"post_mlp_cv": diagnostics[0]["modes"]["learned"][
"positions"
]["post_mlp_state"]["reductions"]["element_rms"][
"statistics"
]["population_cv"],
},
sort_keys=True,
),
flush=True,
)
steps = 0 if args.run_kind == "smoke" else FORMAL_STEPS
training_history: list[dict[str, float | int]] = []
step_times: list[float] = []
if steps:
model.train()
for step in range(1, steps + 1):
lr = parent.learning_rate(step, steps)
for group in optimizer.param_groups:
group["lr"] = lr
inputs, targets = corpus.training_batch(
args.seed, step, TRAIN_BATCH_SIZE
)
optimizer.zero_grad(set_to_none=True)
torch.cuda.synchronize()
started = time.perf_counter()
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
logits, trace = model(inputs)
if trace is not None:
raise RuntimeError("training unexpectedly captured a trace")
loss = parent.cross_entropy(logits, targets)
if not torch.isfinite(loss):
raise RuntimeError(f"non-finite loss at step {step}")
loss.backward()
unclipped_norm = torch.nn.utils.clip_grad_norm_(
model.parameters(), parent.GRAD_CLIP
)
optimizer.step()
torch.cuda.synchronize()
elapsed_ms = (time.perf_counter() - started) * 1000
if step == TIMING_WARMUP:
torch.cuda.reset_peak_memory_stats()
elif step > TIMING_WARMUP:
step_times.append(elapsed_ms)
if step == 1 or step % 10 == 0 or step == steps:
training_history.append(
{
"step": step,
"loss_nats": loss.detach().cpu().item(),
"bits_per_byte": loss.detach().cpu().item() / math.log(2),
"learning_rate": lr,
"unclipped_grad_norm": float(
unclipped_norm.detach().cpu()
),
}
)
if step in DIAGNOSTIC_STEPS:
evaluations.append(
{
"step": step,
**parent.evaluate(
model,
corpus,
VALIDATION_WINDOWS,
EVAL_BATCH_SIZE,
),
}
)
diagnostics.append(
run_diagnostic_bundle(model, corpus, optimizer, step)
)
post = diagnostics[-1]["modes"]["learned"]["positions"][
"post_mlp_state"
]["reductions"]["element_rms"]["statistics"]
print(
json.dumps(
{
"event": "diagnostic",
"step": step,
"seed": args.seed,
"validation_bpc": evaluations[-1][
"bits_per_byte"
],
"post_mlp_cv": post["population_cv"],
"spike_contrast": post["spike_contrast"],
},
sort_keys=True,
),
flush=True,
)
model.train()
timing = {
"warmup_steps_excluded": TIMING_WARMUP,
"measured_steps": len(step_times),
"mean_ms": mean(step_times) if step_times else None,
"median_ms": statistics.median(step_times) if step_times else None,
"p95_ms": (
type7_quantile(torch.tensor(sorted(step_times)), 0.95)
if step_times
else None
),
"peak_allocated_bytes": torch.cuda.max_memory_allocated(),
"peak_reserved_bytes": torch.cuda.max_memory_reserved(),
}
result = {
"schema_version": 1,
"protocol_id": PROTOCOL_ID,
"parent_protocol_id": PARENT_PROTOCOL_ID,
"run_kind": args.run_kind,
"architecture": "block",
"depth": DEPTH,
"seed": args.seed,
"steps": steps,
"batch_size": TRAIN_BATCH_SIZE,
"target_bytes_seen": steps * TRAIN_BATCH_SIZE * CONTEXT,
"manifest": {
"path": str(args.manifest),
"file_sha256": file_sha256(args.manifest),
"parent_path": str(args.parent_manifest),
"parent_file_sha256": file_sha256(args.parent_manifest),
"formal_schedule_sha256": parent_manifest["windows"][
"formal_schedule_sha256"
],
"validation_tensor_sha256": parent_manifest["windows"][
"validation_tensor_sha256"
],
"diagnostic_tensor_sha256": parent_manifest["windows"][
"diagnostic_tensor_sha256"
],
"input_gate_tensor_hashes": input_gate_hashes,
},
"model": {
"layers": DEPTH,
"aggregation_groups": 8,
"blocks_per_group": 4,
"d_model": parent.round04.D_MODEL,
"heads": parent.round04.HEADS,
"d_ff": parent.round04.D_FF,
"parameters": parent.parameter_inventory(model),
},
"optimizer": {
"name": "AdamW",
"betas": list(parent.BETAS),
"epsilon": parent.ADAM_EPS,
"weight_decay_ndim_ge_2": parent.WEIGHT_DECAY,
"peak_lr": parent.PEAK_LR,
"min_lr": parent.MIN_LR,
"warmup_steps": parent.WARMUP_STEPS,
"grad_clip": parent.GRAD_CLIP,
},
"hashes": {
"initial_public_parameter_structure": public_structure_hash,
"initial_public_parameter_tensors": public_tensors,
"initial_public_parameter_elements": public_elements,
"initial_public_parameters": initial_public_hash,
"initial_mixer_parameters": initial_mixer_hash,
"final_public_parameters": parent.named_state_hash(
model, include_mixers=False
),
"final_mixer_parameters": parent.named_state_hash(
model, include_mixers=True
),
"final_model_state": parent.named_state_hash(
model, include_mixers=None
),
"final_optimizer_state": parent.recursive_state_hash(
optimizer.state_dict()
),
},
"evaluations": evaluations,
"diagnostics": diagnostics,
"training_history": training_history,
"timing": timing,
"environment": {
"python": platform.python_version(),
"torch": torch.__version__,
"cuda": torch.version.cuda,
"gpu": torch.cuda.get_device_name(0),
"compute_capability": list(torch.cuda.get_device_capability(0)),
"cublas_workspace_config": os.environ[
"CUBLAS_WORKSPACE_CONFIG"
],
"deterministic_algorithms": (
torch.are_deterministic_algorithms_enabled()
),
"autocast": "cuda-bfloat16-forward-fp32-cross-entropy",
"compile": False,
},
"artifacts": {
"runner_sha256": file_sha256(Path(__file__)),
"protocol_sha256": file_sha256(
repo_root / "research" / "K3_ATTNRES_SPIKE_PROTOCOL.md"
),
"scoping_sha256": file_sha256(
repo_root / "research" / "K3_ATTNRES_SPIKE_SCOPING.md"
),
},
}
if steps:
result["round05_equivalence"] = frozen_training_compare(
result, parent_raw
)
else:
result["round05_equivalence"] = None
result["canonical_sha256_without_self"] = canonical_sha256(result)
args.output.parent.mkdir(parents=True, exist_ok=True)
temporary = args.output.with_suffix(args.output.suffix + ".tmp")
temporary.write_text(
json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True)
+ "\n"
)
os.replace(temporary, args.output)
print(
json.dumps(
{
"output": str(args.output),
"run_kind": args.run_kind,
"seed": args.seed,
"steps": steps,
"final_bpc": evaluations[-1]["bits_per_byte"],
"canonical_sha256": result[
"canonical_sha256_without_self"
],
"timing": timing,
},
ensure_ascii=False,
indent=2,
),
flush=True,
)
if __name__ == "__main__":
main()