Upload folder using huggingface_hub
Browse files- .gitattributes +1 -0
- README.md +36 -0
- best_metrics.json +20 -0
- chat_template.jinja +4 -0
- config.py +56 -0
- data.py +521 -0
- inference.py +546 -0
- initial_latent_z.safetensors +3 -0
- model.py +777 -0
- model.safetensors +3 -0
- sft_b200.yaml +112 -0
- sft_config.json +115 -0
- tokenizer.json +3 -0
- tokenizer_config.json +17 -0
- train.py +1402 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Samantha Thought-Loop SFT
|
| 2 |
+
|
| 3 |
+
- Base model: `google/t5gemma-l-l-prefixlm-it`
|
| 4 |
+
- Cleaned dataset repo: `BRlkl/timed_natural_conversations_5min4`
|
| 5 |
+
- Raw dataset repo: `BRlkl/timed_natural_conversations_5min4`
|
| 6 |
+
- Latent slots: `256`
|
| 7 |
+
- Fixed tick seconds: `1.0`
|
| 8 |
+
- Chunk ticks: `300`
|
| 9 |
+
- Max horizon ticks: `300`
|
| 10 |
+
- Explicit time features: `False`
|
| 11 |
+
- Gate loss mode: `bce`
|
| 12 |
+
- Hazard soft target window: `16`
|
| 13 |
+
- Hazard soft target tau ticks: `4.0`
|
| 14 |
+
- Hazard censor weight: `0.25`
|
| 15 |
+
- TBPTT by bucket: `{}`
|
| 16 |
+
|
| 17 |
+
## Best Validation Metrics
|
| 18 |
+
|
| 19 |
+
- `active_ticks`: `1888.000000`
|
| 20 |
+
- `decoder_token_count`: `2494.000000`
|
| 21 |
+
- `gate_accuracy`: `0.715042`
|
| 22 |
+
- `gate_f1`: `0.154088`
|
| 23 |
+
- `gate_positive_weight`: `16.000000`
|
| 24 |
+
- `gate_precision`: `0.091932`
|
| 25 |
+
- `gate_pred_positive_count`: `533.000000`
|
| 26 |
+
- `gate_recall`: `0.475728`
|
| 27 |
+
- `gate_target_positive_count`: `103.000000`
|
| 28 |
+
- `hazard_censor_count`: `0.000000`
|
| 29 |
+
- `hazard_event_count`: `0.000000`
|
| 30 |
+
- `loss_decoder`: `3.564287`
|
| 31 |
+
- `loss_gate`: `1.241260`
|
| 32 |
+
- `loss_gate_ranking`: `0.000000`
|
| 33 |
+
- `loss_state_delta`: `0.258536`
|
| 34 |
+
- `loss_total`: `4.521086`
|
| 35 |
+
- `soft_target_mass`: `0.000000`
|
| 36 |
+
- `soft_target_positive_count`: `0.000000`
|
best_metrics.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"loss_total": 4.521085739135742,
|
| 3 |
+
"loss_gate": 1.2412604093551636,
|
| 4 |
+
"loss_gate_ranking": 0.0,
|
| 5 |
+
"loss_decoder": 3.564286708831787,
|
| 6 |
+
"loss_state_delta": 0.25853580236434937,
|
| 7 |
+
"gate_accuracy": 0.7150423526763916,
|
| 8 |
+
"gate_precision": 0.09193246066570282,
|
| 9 |
+
"gate_recall": 0.4757281541824341,
|
| 10 |
+
"gate_f1": 0.15408805012702942,
|
| 11 |
+
"gate_target_positive_count": 103.0,
|
| 12 |
+
"gate_pred_positive_count": 533.0,
|
| 13 |
+
"gate_positive_weight": 16.0,
|
| 14 |
+
"hazard_event_count": 0.0,
|
| 15 |
+
"hazard_censor_count": 0.0,
|
| 16 |
+
"soft_target_mass": 0.0,
|
| 17 |
+
"soft_target_positive_count": 0.0,
|
| 18 |
+
"decoder_token_count": 2494.0,
|
| 19 |
+
"active_ticks": 1888.0
|
| 20 |
+
}
|
chat_template.jinja
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if (message['role'] == 'assistant') %}{% set role = 'model' %}{% else %}{% set role = message['role'] %}{% endif %}{{ '<start_of_turn>' + role + '
|
| 2 |
+
' + message['content'] | trim + '<end_of_turn>
|
| 3 |
+
' }}{% endfor %}{% if add_generation_prompt %}{{'<start_of_turn>model
|
| 4 |
+
'}}{% endif %}
|
config.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
from copy import deepcopy
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
import yaml
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def load_config(path: str | Path) -> dict[str, Any]:
|
| 13 |
+
with Path(path).open("r", encoding="utf-8") as handle:
|
| 14 |
+
data = yaml.safe_load(handle)
|
| 15 |
+
if not isinstance(data, dict):
|
| 16 |
+
raise ValueError(f"Config at {path} must deserialize into a mapping.")
|
| 17 |
+
return data
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
| 21 |
+
merged = deepcopy(base)
|
| 22 |
+
for key, value in override.items():
|
| 23 |
+
if isinstance(value, dict) and isinstance(merged.get(key), dict):
|
| 24 |
+
merged[key] = deep_merge(merged[key], value)
|
| 25 |
+
else:
|
| 26 |
+
merged[key] = value
|
| 27 |
+
return merged
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def ensure_dir(path: str | Path) -> Path:
|
| 31 |
+
resolved = Path(path)
|
| 32 |
+
resolved.mkdir(parents=True, exist_ok=True)
|
| 33 |
+
return resolved
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def save_json(path: str | Path, payload: dict[str, Any]) -> None:
|
| 37 |
+
target = Path(path)
|
| 38 |
+
target.parent.mkdir(parents=True, exist_ok=True)
|
| 39 |
+
with target.open("w", encoding="utf-8") as handle:
|
| 40 |
+
json.dump(payload, handle, indent=2, ensure_ascii=False)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def flatten_for_wandb(payload: dict[str, Any], prefix: str = "") -> dict[str, Any]:
|
| 44 |
+
flat: dict[str, Any] = {}
|
| 45 |
+
for key, value in payload.items():
|
| 46 |
+
full_key = f"{prefix}.{key}" if prefix else key
|
| 47 |
+
if isinstance(value, dict):
|
| 48 |
+
flat.update(flatten_for_wandb(value, prefix=full_key))
|
| 49 |
+
else:
|
| 50 |
+
flat[full_key] = value
|
| 51 |
+
return flat
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def fingerprint_payload(payload: dict[str, Any]) -> str:
|
| 55 |
+
serialized = json.dumps(payload, sort_keys=True, ensure_ascii=True, separators=(",", ":"))
|
| 56 |
+
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()[:16]
|
data.py
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import math
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from datasets import Dataset as HFDataset
|
| 11 |
+
from datasets import load_dataset
|
| 12 |
+
from torch.utils.data import BatchSampler, Dataset
|
| 13 |
+
|
| 14 |
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
| 15 |
+
if str(SCRIPT_DIR) not in sys.path:
|
| 16 |
+
sys.path.insert(0, str(SCRIPT_DIR))
|
| 17 |
+
|
| 18 |
+
from config import ensure_dir, fingerprint_payload
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
OBS_ROLE_NONE = 0
|
| 22 |
+
OBS_ROLE_USER = 1
|
| 23 |
+
OBS_ROLE_AGENT_FEEDBACK = 2
|
| 24 |
+
SFT_CACHE_SCHEMA_VERSION = 2
|
| 25 |
+
|
| 26 |
+
DURATION_BUCKET_ORDER = {
|
| 27 |
+
"short": 0,
|
| 28 |
+
"medium": 1,
|
| 29 |
+
"long": 2,
|
| 30 |
+
"hour_scale": 3,
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def load_clean_split(dataset_cfg: dict[str, Any], split: str) -> HFDataset:
|
| 35 |
+
local_parquet_dir = dataset_cfg.get("cleaned_local_parquet_dir")
|
| 36 |
+
if local_parquet_dir and Path(local_parquet_dir).exists():
|
| 37 |
+
parquet_path = Path(local_parquet_dir) / f"{split}.parquet"
|
| 38 |
+
if parquet_path.exists():
|
| 39 |
+
return load_dataset("parquet", data_files={split: str(parquet_path)}, split=split)
|
| 40 |
+
|
| 41 |
+
cleaned_repo_id = dataset_cfg.get("cleaned_repo_id")
|
| 42 |
+
if not cleaned_repo_id:
|
| 43 |
+
raise ValueError("Dataset config must provide either cleaned_local_parquet_dir or cleaned_repo_id.")
|
| 44 |
+
|
| 45 |
+
source_split = dataset_cfg.get("source_split")
|
| 46 |
+
if source_split:
|
| 47 |
+
dataset = load_dataset(cleaned_repo_id, split=str(source_split))
|
| 48 |
+
if split not in {"train", "validation"}:
|
| 49 |
+
return dataset
|
| 50 |
+
|
| 51 |
+
validation_ratio = float(dataset_cfg.get("validation_ratio", 0.02))
|
| 52 |
+
split_seed = int(dataset_cfg.get("split_seed", 17))
|
| 53 |
+
selected_indices = [
|
| 54 |
+
index
|
| 55 |
+
for index, row in enumerate(dataset)
|
| 56 |
+
if assign_deterministic_split(str(row["id"]), validation_ratio, split_seed) == split
|
| 57 |
+
]
|
| 58 |
+
return dataset.select(selected_indices)
|
| 59 |
+
|
| 60 |
+
return load_dataset(cleaned_repo_id, split=split)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def tokenize_text(tokenizer: Any, text: str | None, max_length: int) -> tuple[list[int], list[int]]:
|
| 64 |
+
if not text:
|
| 65 |
+
return [tokenizer.pad_token_id], [0]
|
| 66 |
+
encoded = tokenizer(
|
| 67 |
+
text,
|
| 68 |
+
add_special_tokens=True,
|
| 69 |
+
truncation=True,
|
| 70 |
+
max_length=max_length,
|
| 71 |
+
)
|
| 72 |
+
return encoded["input_ids"], encoded["attention_mask"]
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def tokenizer_supports_chat_template(tokenizer: Any) -> bool:
|
| 76 |
+
return bool(getattr(tokenizer, "chat_template", None))
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def should_use_base_chat_template(config: dict[str, Any], tokenizer: Any) -> bool:
|
| 80 |
+
dataset_cfg = config.get("dataset", {})
|
| 81 |
+
inference_cfg = config.get("inference", {})
|
| 82 |
+
if "use_base_chat_template" in inference_cfg:
|
| 83 |
+
return bool(inference_cfg["use_base_chat_template"]) and tokenizer_supports_chat_template(tokenizer)
|
| 84 |
+
return bool(dataset_cfg.get("use_base_chat_template", False)) and tokenizer_supports_chat_template(tokenizer)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def build_user_generation_observation(tokenizer: Any, text: str, use_base_chat_template: bool) -> str:
|
| 88 |
+
if use_base_chat_template:
|
| 89 |
+
return tokenizer.apply_chat_template(
|
| 90 |
+
[{"role": "user", "content": text}],
|
| 91 |
+
tokenize=False,
|
| 92 |
+
add_generation_prompt=True,
|
| 93 |
+
)
|
| 94 |
+
return text
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def build_assistant_feedback_observation(tokenizer: Any, text: str, use_base_chat_template: bool) -> str:
|
| 98 |
+
del tokenizer
|
| 99 |
+
if use_base_chat_template:
|
| 100 |
+
return f"<start_of_turn>model\n{text}<end_of_turn>\n"
|
| 101 |
+
return text
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def build_assistant_decoder_target(tokenizer: Any, text: str, use_base_chat_template: bool) -> str:
|
| 105 |
+
if not use_base_chat_template:
|
| 106 |
+
return text
|
| 107 |
+
|
| 108 |
+
prompt_text = tokenizer.apply_chat_template(
|
| 109 |
+
[{"role": "user", "content": ""}],
|
| 110 |
+
tokenize=False,
|
| 111 |
+
add_generation_prompt=True,
|
| 112 |
+
)
|
| 113 |
+
full_text = tokenizer.apply_chat_template(
|
| 114 |
+
[
|
| 115 |
+
{"role": "user", "content": ""},
|
| 116 |
+
{"role": "assistant", "content": text},
|
| 117 |
+
],
|
| 118 |
+
tokenize=False,
|
| 119 |
+
add_generation_prompt=False,
|
| 120 |
+
)
|
| 121 |
+
if not full_text.startswith(prompt_text):
|
| 122 |
+
raise ValueError("Chat template full text did not start with the generation prompt prefix.")
|
| 123 |
+
return full_text[len(prompt_text) :]
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def duration_bucket_rank(bucket: str | None) -> int:
|
| 127 |
+
return DURATION_BUCKET_ORDER.get(str(bucket), len(DURATION_BUCKET_ORDER))
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def assign_deterministic_split(row_id: str, validation_ratio: float, seed: int) -> str:
|
| 131 |
+
digest = hashlib.sha1(f"{seed}:{row_id}".encode("utf-8")).hexdigest()
|
| 132 |
+
score = int(digest[:8], 16) / 0xFFFFFFFF
|
| 133 |
+
return "validation" if score < validation_ratio else "train"
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def stable_tick_index(time_seconds: float, tick_seconds: float) -> int:
|
| 137 |
+
ratio = max(0.0, float(time_seconds)) / tick_seconds
|
| 138 |
+
return int(math.ceil(ratio - 1e-9))
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def quantize_message_sequence(
|
| 142 |
+
messages: list[dict[str, Any]],
|
| 143 |
+
*,
|
| 144 |
+
tick_seconds: float,
|
| 145 |
+
feedback_delay_seconds: float,
|
| 146 |
+
) -> list[dict[str, Any]]:
|
| 147 |
+
raw_events: list[dict[str, Any]] = []
|
| 148 |
+
event_order = 0
|
| 149 |
+
for message in messages:
|
| 150 |
+
speaker = str(message["speaker"])
|
| 151 |
+
text = str(message["text"])
|
| 152 |
+
time_seconds = float(message["t"])
|
| 153 |
+
|
| 154 |
+
if speaker == "user":
|
| 155 |
+
raw_events.append(
|
| 156 |
+
{
|
| 157 |
+
"time_seconds": time_seconds,
|
| 158 |
+
"order": event_order,
|
| 159 |
+
"kind": "user",
|
| 160 |
+
"text": text,
|
| 161 |
+
}
|
| 162 |
+
)
|
| 163 |
+
event_order += 1
|
| 164 |
+
continue
|
| 165 |
+
|
| 166 |
+
raw_events.append(
|
| 167 |
+
{
|
| 168 |
+
"time_seconds": time_seconds,
|
| 169 |
+
"order": event_order,
|
| 170 |
+
"kind": "agent_speak",
|
| 171 |
+
"text": text,
|
| 172 |
+
}
|
| 173 |
+
)
|
| 174 |
+
event_order += 1
|
| 175 |
+
raw_events.append(
|
| 176 |
+
{
|
| 177 |
+
"time_seconds": time_seconds + feedback_delay_seconds,
|
| 178 |
+
"order": event_order,
|
| 179 |
+
"kind": "agent_feedback",
|
| 180 |
+
"text": text,
|
| 181 |
+
}
|
| 182 |
+
)
|
| 183 |
+
event_order += 1
|
| 184 |
+
|
| 185 |
+
raw_events.sort(key=lambda item: (float(item["time_seconds"]), int(item["order"])))
|
| 186 |
+
|
| 187 |
+
quantized_events: list[dict[str, Any]] = []
|
| 188 |
+
previous_tick = -1
|
| 189 |
+
for event in raw_events:
|
| 190 |
+
desired_tick = stable_tick_index(float(event["time_seconds"]), tick_seconds)
|
| 191 |
+
assigned_tick = max(desired_tick, previous_tick + 1)
|
| 192 |
+
quantized_events.append(
|
| 193 |
+
{
|
| 194 |
+
"tick": assigned_tick,
|
| 195 |
+
"kind": event["kind"],
|
| 196 |
+
"text": event["text"],
|
| 197 |
+
}
|
| 198 |
+
)
|
| 199 |
+
previous_tick = assigned_tick
|
| 200 |
+
|
| 201 |
+
return quantized_events
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def resolve_bucket_horizon_ticks(duration_bucket: str, rollout_cfg: dict[str, Any]) -> int:
|
| 205 |
+
bucket_map = rollout_cfg.get("horizon_ticks_by_bucket", {})
|
| 206 |
+
default_horizon = int(rollout_cfg.get("max_horizon_ticks", 36000))
|
| 207 |
+
return int(bucket_map.get(duration_bucket, default_horizon))
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def build_fixed_tick_conversation(
|
| 211 |
+
*,
|
| 212 |
+
row: dict[str, Any],
|
| 213 |
+
tokenizer: Any,
|
| 214 |
+
config: dict[str, Any],
|
| 215 |
+
rollout_cfg: dict[str, Any],
|
| 216 |
+
max_observation_tokens: int,
|
| 217 |
+
max_decoder_tokens: int,
|
| 218 |
+
) -> dict[str, Any] | None:
|
| 219 |
+
tick_seconds = float(rollout_cfg["tick_seconds"])
|
| 220 |
+
chunk_ticks = int(rollout_cfg["chunk_ticks"])
|
| 221 |
+
feedback_delay_seconds = float(rollout_cfg["post_speech_feedback_delay_seconds"])
|
| 222 |
+
window_strategy = str(rollout_cfg.get("long_window_strategy", "tail"))
|
| 223 |
+
duration_bucket = str(row.get("meta", {}).get("duration_bucket", "short"))
|
| 224 |
+
use_base_chat_template = should_use_base_chat_template(config, tokenizer)
|
| 225 |
+
|
| 226 |
+
quantized_events = quantize_message_sequence(
|
| 227 |
+
messages=row["messages"],
|
| 228 |
+
tick_seconds=tick_seconds,
|
| 229 |
+
feedback_delay_seconds=feedback_delay_seconds,
|
| 230 |
+
)
|
| 231 |
+
total_ticks = max((event["tick"] for event in quantized_events), default=0) + 1
|
| 232 |
+
|
| 233 |
+
horizon_ticks = resolve_bucket_horizon_ticks(duration_bucket, rollout_cfg)
|
| 234 |
+
if bool(rollout_cfg.get("drop_overlong_examples", False)) and total_ticks > horizon_ticks:
|
| 235 |
+
return None
|
| 236 |
+
|
| 237 |
+
if total_ticks <= horizon_ticks:
|
| 238 |
+
effective_start_tick = 0
|
| 239 |
+
effective_total_ticks = total_ticks
|
| 240 |
+
elif window_strategy == "tail":
|
| 241 |
+
effective_start_tick = total_ticks - horizon_ticks
|
| 242 |
+
effective_total_ticks = horizon_ticks
|
| 243 |
+
else:
|
| 244 |
+
effective_start_tick = 0
|
| 245 |
+
effective_total_ticks = horizon_ticks
|
| 246 |
+
|
| 247 |
+
chunk_count = max(1, math.ceil(effective_total_ticks / chunk_ticks))
|
| 248 |
+
chunk_lengths = [
|
| 249 |
+
min(chunk_ticks, max(0, effective_total_ticks - chunk_index * chunk_ticks))
|
| 250 |
+
for chunk_index in range(chunk_count)
|
| 251 |
+
]
|
| 252 |
+
chunk_events: list[list[dict[str, Any]]] = [[] for _ in range(chunk_count)]
|
| 253 |
+
|
| 254 |
+
event_count = 0
|
| 255 |
+
for event in quantized_events:
|
| 256 |
+
absolute_tick = int(event["tick"])
|
| 257 |
+
if absolute_tick < effective_start_tick or absolute_tick >= effective_start_tick + effective_total_ticks:
|
| 258 |
+
continue
|
| 259 |
+
|
| 260 |
+
relative_tick = absolute_tick - effective_start_tick
|
| 261 |
+
chunk_index = relative_tick // chunk_ticks
|
| 262 |
+
offset = relative_tick % chunk_ticks
|
| 263 |
+
|
| 264 |
+
if event["kind"] == "user":
|
| 265 |
+
observation_role = OBS_ROLE_USER
|
| 266 |
+
observation_text = build_user_generation_observation(
|
| 267 |
+
tokenizer,
|
| 268 |
+
str(event["text"]),
|
| 269 |
+
use_base_chat_template,
|
| 270 |
+
)
|
| 271 |
+
gate_target = 0
|
| 272 |
+
decoder_text = None
|
| 273 |
+
elif event["kind"] == "agent_feedback":
|
| 274 |
+
observation_role = OBS_ROLE_AGENT_FEEDBACK
|
| 275 |
+
observation_text = build_assistant_feedback_observation(
|
| 276 |
+
tokenizer,
|
| 277 |
+
str(event["text"]),
|
| 278 |
+
use_base_chat_template,
|
| 279 |
+
)
|
| 280 |
+
gate_target = 0
|
| 281 |
+
decoder_text = None
|
| 282 |
+
else:
|
| 283 |
+
observation_role = OBS_ROLE_NONE
|
| 284 |
+
observation_text = None
|
| 285 |
+
gate_target = 1
|
| 286 |
+
decoder_text = build_assistant_decoder_target(
|
| 287 |
+
tokenizer,
|
| 288 |
+
str(event["text"]),
|
| 289 |
+
use_base_chat_template,
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
observation_input_ids, observation_attention_mask = tokenize_text(
|
| 293 |
+
tokenizer=tokenizer,
|
| 294 |
+
text=observation_text,
|
| 295 |
+
max_length=max_observation_tokens,
|
| 296 |
+
)
|
| 297 |
+
decoder_labels, _ = tokenize_text(
|
| 298 |
+
tokenizer=tokenizer,
|
| 299 |
+
text=decoder_text,
|
| 300 |
+
max_length=max_decoder_tokens,
|
| 301 |
+
)
|
| 302 |
+
if gate_target == 0:
|
| 303 |
+
decoder_labels = []
|
| 304 |
+
|
| 305 |
+
chunk_events[chunk_index].append(
|
| 306 |
+
{
|
| 307 |
+
"offset": int(offset),
|
| 308 |
+
"absolute_tick": absolute_tick,
|
| 309 |
+
"observation_role": int(observation_role),
|
| 310 |
+
"observation_input_ids": observation_input_ids,
|
| 311 |
+
"observation_attention_mask": observation_attention_mask,
|
| 312 |
+
"gate_target": int(gate_target),
|
| 313 |
+
"decoder_labels": decoder_labels,
|
| 314 |
+
}
|
| 315 |
+
)
|
| 316 |
+
event_count += 1
|
| 317 |
+
|
| 318 |
+
return {
|
| 319 |
+
"row_id": str(row["id"]),
|
| 320 |
+
"scenario_id": row.get("meta", {}).get("scenario_id"),
|
| 321 |
+
"duration_bucket": duration_bucket,
|
| 322 |
+
"bucket_rank": duration_bucket_rank(duration_bucket),
|
| 323 |
+
"total_ticks": int(total_ticks),
|
| 324 |
+
"effective_start_tick": int(effective_start_tick),
|
| 325 |
+
"effective_total_ticks": int(effective_total_ticks),
|
| 326 |
+
"chunk_count": int(chunk_count),
|
| 327 |
+
"chunk_lengths": chunk_lengths,
|
| 328 |
+
"chunk_events": chunk_events,
|
| 329 |
+
"event_count": int(event_count),
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def build_chunk_batch(
|
| 334 |
+
*,
|
| 335 |
+
conversations: list[dict[str, Any]],
|
| 336 |
+
chunk_index: int,
|
| 337 |
+
pad_token_id: int,
|
| 338 |
+
chunk_ticks: int,
|
| 339 |
+
tick_seconds: float,
|
| 340 |
+
) -> dict[str, torch.Tensor] | None:
|
| 341 |
+
active_lengths: list[int] = []
|
| 342 |
+
max_observation_tokens = 1
|
| 343 |
+
max_decoder_tokens = 1
|
| 344 |
+
|
| 345 |
+
for conversation in conversations:
|
| 346 |
+
if chunk_index >= int(conversation["chunk_count"]):
|
| 347 |
+
active_lengths.append(0)
|
| 348 |
+
continue
|
| 349 |
+
step_count = int(conversation["chunk_lengths"][chunk_index])
|
| 350 |
+
active_lengths.append(step_count)
|
| 351 |
+
for event in conversation["chunk_events"][chunk_index]:
|
| 352 |
+
max_observation_tokens = max(max_observation_tokens, len(event["observation_input_ids"]))
|
| 353 |
+
max_decoder_tokens = max(max_decoder_tokens, len(event["decoder_labels"]) or 1)
|
| 354 |
+
|
| 355 |
+
max_steps = max(active_lengths, default=0)
|
| 356 |
+
if max_steps <= 0:
|
| 357 |
+
return None
|
| 358 |
+
|
| 359 |
+
batch_size = len(conversations)
|
| 360 |
+
observation_input_ids = torch.full(
|
| 361 |
+
(batch_size, max_steps, max_observation_tokens),
|
| 362 |
+
fill_value=pad_token_id,
|
| 363 |
+
dtype=torch.long,
|
| 364 |
+
)
|
| 365 |
+
observation_attention_mask = torch.zeros((batch_size, max_steps, max_observation_tokens), dtype=torch.long)
|
| 366 |
+
decoder_labels = torch.full((batch_size, max_steps, max_decoder_tokens), fill_value=-100, dtype=torch.long)
|
| 367 |
+
tick_mask = torch.zeros((batch_size, max_steps), dtype=torch.bool)
|
| 368 |
+
gate_target = torch.zeros((batch_size, max_steps), dtype=torch.float32)
|
| 369 |
+
observation_role = torch.zeros((batch_size, max_steps), dtype=torch.long)
|
| 370 |
+
delta_seconds = torch.zeros((batch_size, max_steps), dtype=torch.float32)
|
| 371 |
+
elapsed_seconds = torch.zeros((batch_size, max_steps), dtype=torch.float32)
|
| 372 |
+
|
| 373 |
+
for batch_index, conversation in enumerate(conversations):
|
| 374 |
+
step_count = active_lengths[batch_index]
|
| 375 |
+
if step_count <= 0:
|
| 376 |
+
continue
|
| 377 |
+
|
| 378 |
+
tick_mask[batch_index, :step_count] = True
|
| 379 |
+
for local_tick in range(step_count):
|
| 380 |
+
global_tick = chunk_index * chunk_ticks + local_tick
|
| 381 |
+
delta_seconds[batch_index, local_tick] = 0.0 if global_tick == 0 else tick_seconds
|
| 382 |
+
elapsed_seconds[batch_index, local_tick] = global_tick * tick_seconds
|
| 383 |
+
|
| 384 |
+
for event in conversation["chunk_events"][chunk_index]:
|
| 385 |
+
offset = int(event["offset"])
|
| 386 |
+
observation_role[batch_index, offset] = int(event["observation_role"])
|
| 387 |
+
gate_target[batch_index, offset] = float(event["gate_target"])
|
| 388 |
+
|
| 389 |
+
observation_ids = event["observation_input_ids"]
|
| 390 |
+
observation_mask = event["observation_attention_mask"]
|
| 391 |
+
observation_input_ids[batch_index, offset, : len(observation_ids)] = torch.tensor(
|
| 392 |
+
observation_ids,
|
| 393 |
+
dtype=torch.long,
|
| 394 |
+
)
|
| 395 |
+
observation_attention_mask[batch_index, offset, : len(observation_mask)] = torch.tensor(
|
| 396 |
+
observation_mask,
|
| 397 |
+
dtype=torch.long,
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
labels = event["decoder_labels"]
|
| 401 |
+
if labels:
|
| 402 |
+
decoder_labels[batch_index, offset, : len(labels)] = torch.tensor(labels, dtype=torch.long)
|
| 403 |
+
|
| 404 |
+
return {
|
| 405 |
+
"tick_mask": tick_mask,
|
| 406 |
+
"gate_target": gate_target,
|
| 407 |
+
"observation_role": observation_role,
|
| 408 |
+
"observation_input_ids": observation_input_ids,
|
| 409 |
+
"observation_attention_mask": observation_attention_mask,
|
| 410 |
+
"decoder_labels": decoder_labels,
|
| 411 |
+
"delta_seconds": delta_seconds,
|
| 412 |
+
"elapsed_seconds": elapsed_seconds,
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
class ThoughtLoopConversationDataset(Dataset):
|
| 417 |
+
def __init__(self, config: dict[str, Any], tokenizer: Any, split: str) -> None:
|
| 418 |
+
self.config = config
|
| 419 |
+
self.tokenizer = tokenizer
|
| 420 |
+
self.split = split
|
| 421 |
+
self.examples = self._load_or_build()
|
| 422 |
+
|
| 423 |
+
def _cache_path(self) -> Path:
|
| 424 |
+
dataset_cfg = self.config["dataset"]
|
| 425 |
+
model_cfg = self.config["model"]
|
| 426 |
+
rollout_cfg = self.config["rollout"]
|
| 427 |
+
cache_cfg = self.config["cache"]
|
| 428 |
+
cache_root = ensure_dir(cache_cfg["preprocessed_root"])
|
| 429 |
+
payload = {
|
| 430 |
+
"cache_schema_version": SFT_CACHE_SCHEMA_VERSION,
|
| 431 |
+
"split": self.split,
|
| 432 |
+
"dataset": dataset_cfg,
|
| 433 |
+
"model_name": model_cfg["base_model_name"],
|
| 434 |
+
"rollout": rollout_cfg,
|
| 435 |
+
"max_observation_tokens": model_cfg["max_observation_tokens"],
|
| 436 |
+
"max_decoder_tokens": model_cfg["max_decoder_tokens"],
|
| 437 |
+
"tokenizer_vocab_size": getattr(self.tokenizer, "vocab_size", None),
|
| 438 |
+
}
|
| 439 |
+
return cache_root / f"{self.split}_{fingerprint_payload(payload)}.pt"
|
| 440 |
+
|
| 441 |
+
def _load_or_build(self) -> list[dict[str, Any]]:
|
| 442 |
+
cache_path = self._cache_path()
|
| 443 |
+
if cache_path.exists():
|
| 444 |
+
return torch.load(cache_path, map_location="cpu")
|
| 445 |
+
|
| 446 |
+
dataset = load_clean_split(self.config["dataset"], self.split)
|
| 447 |
+
rollout_cfg = self.config["rollout"]
|
| 448 |
+
model_cfg = self.config["model"]
|
| 449 |
+
|
| 450 |
+
examples: list[dict[str, Any]] = []
|
| 451 |
+
for row in dataset:
|
| 452 |
+
example = build_fixed_tick_conversation(
|
| 453 |
+
row=row,
|
| 454 |
+
tokenizer=self.tokenizer,
|
| 455 |
+
config=self.config,
|
| 456 |
+
rollout_cfg=rollout_cfg,
|
| 457 |
+
max_observation_tokens=int(model_cfg["max_observation_tokens"]),
|
| 458 |
+
max_decoder_tokens=int(model_cfg["max_decoder_tokens"]),
|
| 459 |
+
)
|
| 460 |
+
if example is not None:
|
| 461 |
+
examples.append(example)
|
| 462 |
+
|
| 463 |
+
if bool(rollout_cfg.get("sort_by_duration_bucket", True)):
|
| 464 |
+
examples.sort(key=lambda item: (int(item["bucket_rank"]), int(item["total_ticks"]), str(item["row_id"])))
|
| 465 |
+
|
| 466 |
+
torch.save(examples, cache_path)
|
| 467 |
+
return examples
|
| 468 |
+
|
| 469 |
+
def __len__(self) -> int:
|
| 470 |
+
return len(self.examples)
|
| 471 |
+
|
| 472 |
+
def __getitem__(self, index: int) -> dict[str, Any]:
|
| 473 |
+
return self.examples[index]
|
| 474 |
+
|
| 475 |
+
|
| 476 |
+
class DurationBucketBatchSampler(BatchSampler):
|
| 477 |
+
def __init__(
|
| 478 |
+
self,
|
| 479 |
+
dataset: ThoughtLoopConversationDataset,
|
| 480 |
+
batch_size: int,
|
| 481 |
+
) -> None:
|
| 482 |
+
self.dataset = dataset
|
| 483 |
+
self.batch_size = batch_size
|
| 484 |
+
self.batches: list[list[int]] = []
|
| 485 |
+
|
| 486 |
+
current_bucket: str | None = None
|
| 487 |
+
current_batch: list[int] = []
|
| 488 |
+
for index, example in enumerate(self.dataset.examples):
|
| 489 |
+
bucket = str(example["duration_bucket"])
|
| 490 |
+
if current_batch and (bucket != current_bucket or len(current_batch) >= self.batch_size):
|
| 491 |
+
self.batches.append(current_batch)
|
| 492 |
+
current_batch = []
|
| 493 |
+
current_bucket = bucket
|
| 494 |
+
current_batch.append(index)
|
| 495 |
+
if len(current_batch) >= self.batch_size:
|
| 496 |
+
self.batches.append(current_batch)
|
| 497 |
+
current_batch = []
|
| 498 |
+
|
| 499 |
+
if current_batch:
|
| 500 |
+
self.batches.append(current_batch)
|
| 501 |
+
|
| 502 |
+
def __iter__(self) -> Any:
|
| 503 |
+
return iter(self.batches)
|
| 504 |
+
|
| 505 |
+
def __len__(self) -> int:
|
| 506 |
+
return len(self.batches)
|
| 507 |
+
|
| 508 |
+
|
| 509 |
+
def identity_collate(batch: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 510 |
+
return batch
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
def estimate_total_chunk_microsteps(
|
| 514 |
+
*,
|
| 515 |
+
dataset: ThoughtLoopConversationDataset,
|
| 516 |
+
batch_sampler: DurationBucketBatchSampler,
|
| 517 |
+
) -> int:
|
| 518 |
+
total_microsteps = 0
|
| 519 |
+
for batch_indices in batch_sampler.batches:
|
| 520 |
+
total_microsteps += max(int(dataset.examples[index]["chunk_count"]) for index in batch_indices)
|
| 521 |
+
return total_microsteps
|
inference.py
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
from collections import deque
|
| 5 |
+
import json
|
| 6 |
+
import math
|
| 7 |
+
import sys
|
| 8 |
+
import threading
|
| 9 |
+
import time
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
|
| 15 |
+
|
| 16 |
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
| 17 |
+
if str(SCRIPT_DIR) not in sys.path:
|
| 18 |
+
sys.path.insert(0, str(SCRIPT_DIR))
|
| 19 |
+
|
| 20 |
+
from data import (
|
| 21 |
+
OBS_ROLE_AGENT_FEEDBACK,
|
| 22 |
+
OBS_ROLE_NONE,
|
| 23 |
+
OBS_ROLE_USER,
|
| 24 |
+
build_assistant_feedback_observation,
|
| 25 |
+
build_user_generation_observation,
|
| 26 |
+
quantize_message_sequence,
|
| 27 |
+
should_use_base_chat_template,
|
| 28 |
+
tokenize_text,
|
| 29 |
+
)
|
| 30 |
+
from model import ThoughtLoopT5Gemma
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def parse_args() -> argparse.Namespace:
|
| 34 |
+
parser = argparse.ArgumentParser(
|
| 35 |
+
description="Interactive inference for Samantha thought-loop checkpoints, with a direct HF baseline fallback."
|
| 36 |
+
)
|
| 37 |
+
parser.add_argument("--model-path", required=True, help="Local export directory or Hugging Face repo id.")
|
| 38 |
+
parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
| 39 |
+
parser.add_argument("--history-json", default=None, help="Optional JSON array of {t, speaker, text} turns to replay.")
|
| 40 |
+
parser.add_argument("--user", default=None, help="Optional single user message to answer.")
|
| 41 |
+
parser.add_argument("--interactive", action="store_true", help="Run a live terminal session.")
|
| 42 |
+
parser.add_argument("--tick-seconds", type=float, default=None, help="Runtime tick size. Defaults to the trained value.")
|
| 43 |
+
parser.add_argument("--max-thought-seconds", type=float, default=300.0)
|
| 44 |
+
parser.add_argument("--settle-thought-seconds", type=float, default=1.0)
|
| 45 |
+
parser.add_argument("--max-autonomous-utterances", type=int, default=4, help="One-shot --user safety cap.")
|
| 46 |
+
parser.add_argument("--gate-threshold", type=float, default=0.5, help="Trained binary gate decision threshold.")
|
| 47 |
+
parser.add_argument("--max-new-tokens", type=int, default=160)
|
| 48 |
+
parser.add_argument("--temperature", type=float, default=0.8)
|
| 49 |
+
parser.add_argument("--top-p", type=float, default=0.95)
|
| 50 |
+
parser.add_argument(
|
| 51 |
+
"--debug-gate",
|
| 52 |
+
"--show-gate",
|
| 53 |
+
dest="debug_gate",
|
| 54 |
+
action="store_true",
|
| 55 |
+
help="Print the gate head probability on every runtime tick.",
|
| 56 |
+
)
|
| 57 |
+
return parser.parse_args()
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def build_generation_kwargs(args: argparse.Namespace) -> dict[str, Any]:
|
| 61 |
+
kwargs: dict[str, Any] = {
|
| 62 |
+
"max_new_tokens": int(args.max_new_tokens),
|
| 63 |
+
"do_sample": float(args.temperature) > 0.0,
|
| 64 |
+
}
|
| 65 |
+
if kwargs["do_sample"]:
|
| 66 |
+
kwargs["temperature"] = float(args.temperature)
|
| 67 |
+
kwargs["top_p"] = float(args.top_p)
|
| 68 |
+
return kwargs
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def normalize_history_speaker(speaker: str | None) -> str | None:
|
| 72 |
+
normalized = str(speaker or "").strip().lower()
|
| 73 |
+
if normalized in {"user", "human", "prompter"}:
|
| 74 |
+
return "user"
|
| 75 |
+
if normalized in {"assistant", "agent", "model", "gpt"}:
|
| 76 |
+
return "assistant"
|
| 77 |
+
return None
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def load_base_seq2seq_model(
|
| 81 |
+
model_path: str,
|
| 82 |
+
*,
|
| 83 |
+
device: str | torch.device,
|
| 84 |
+
) -> tuple[torch.nn.Module, Any]:
|
| 85 |
+
resolved_device = torch.device(device)
|
| 86 |
+
dtype = torch.bfloat16 if resolved_device.type == "cuda" else torch.float32
|
| 87 |
+
model = AutoModelForSeq2SeqLM.from_pretrained(model_path, torch_dtype=dtype)
|
| 88 |
+
tokenizer = AutoTokenizer.from_pretrained(model_path)
|
| 89 |
+
model.to(resolved_device)
|
| 90 |
+
model.eval()
|
| 91 |
+
return model, tokenizer
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def load_inference_backend(
|
| 95 |
+
model_path: str,
|
| 96 |
+
*,
|
| 97 |
+
device: str | torch.device,
|
| 98 |
+
) -> tuple[str, Any, Any | None]:
|
| 99 |
+
try:
|
| 100 |
+
model = ThoughtLoopT5Gemma.from_pretrained(model_path, device=device, map_location=device)
|
| 101 |
+
model.eval()
|
| 102 |
+
return "thought_loop", model, None
|
| 103 |
+
except FileNotFoundError as exc:
|
| 104 |
+
if Path(str(exc.filename)).name != "sft_config.json":
|
| 105 |
+
raise
|
| 106 |
+
print(
|
| 107 |
+
"[inference] No sft_config.json found at model path; loading as a raw Hugging Face seq2seq baseline.",
|
| 108 |
+
file=sys.stderr,
|
| 109 |
+
)
|
| 110 |
+
base_model, tokenizer = load_base_seq2seq_model(model_path, device=device)
|
| 111 |
+
return "base_seq2seq", base_model, tokenizer
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
class BaseSeq2SeqSession:
|
| 115 |
+
def __init__(
|
| 116 |
+
self,
|
| 117 |
+
model: torch.nn.Module,
|
| 118 |
+
tokenizer: Any,
|
| 119 |
+
device: str | torch.device,
|
| 120 |
+
generation_kwargs: dict[str, Any],
|
| 121 |
+
) -> None:
|
| 122 |
+
self.model = model
|
| 123 |
+
self.tokenizer = tokenizer
|
| 124 |
+
self.device = torch.device(device)
|
| 125 |
+
self.generation_kwargs = generation_kwargs
|
| 126 |
+
self.history: list[dict[str, str]] = []
|
| 127 |
+
|
| 128 |
+
def replay_history(self, messages: list[dict[str, Any]]) -> None:
|
| 129 |
+
ordered_messages = sorted(messages, key=lambda item: float(item.get("t", 0.0)))
|
| 130 |
+
for message in ordered_messages:
|
| 131 |
+
role = normalize_history_speaker(message.get("speaker"))
|
| 132 |
+
text = str(message.get("text", "")).strip()
|
| 133 |
+
if role is None or not text:
|
| 134 |
+
continue
|
| 135 |
+
self.history.append({"role": role, "content": text})
|
| 136 |
+
|
| 137 |
+
def _format_prompt(self, user_text: str) -> str:
|
| 138 |
+
messages = [*self.history, {"role": "user", "content": user_text}]
|
| 139 |
+
if getattr(self.tokenizer, "chat_template", None):
|
| 140 |
+
return self.tokenizer.apply_chat_template(
|
| 141 |
+
messages,
|
| 142 |
+
tokenize=False,
|
| 143 |
+
add_generation_prompt=True,
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
lines: list[str] = []
|
| 147 |
+
for message in messages:
|
| 148 |
+
role = "User" if message["role"] == "user" else "Assistant"
|
| 149 |
+
lines.append(f"{role}: {message['content']}")
|
| 150 |
+
lines.append("Assistant:")
|
| 151 |
+
return "\n".join(lines)
|
| 152 |
+
|
| 153 |
+
def respond(
|
| 154 |
+
self,
|
| 155 |
+
user_text: str,
|
| 156 |
+
*,
|
| 157 |
+
max_thought_seconds: float,
|
| 158 |
+
settle_thought_seconds: float,
|
| 159 |
+
max_autonomous_utterances: int,
|
| 160 |
+
) -> tuple[list[str], float]:
|
| 161 |
+
del max_thought_seconds, settle_thought_seconds, max_autonomous_utterances
|
| 162 |
+
prompt = self._format_prompt(user_text)
|
| 163 |
+
encoded = self.tokenizer(prompt, return_tensors="pt").to(self.device)
|
| 164 |
+
with torch.no_grad():
|
| 165 |
+
generated = self.model.generate(**encoded, **self.generation_kwargs)
|
| 166 |
+
response = self.tokenizer.decode(generated[0], skip_special_tokens=True).strip()
|
| 167 |
+
self.history.append({"role": "user", "content": user_text})
|
| 168 |
+
self.history.append({"role": "assistant", "content": response})
|
| 169 |
+
return [response], 0.0
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
class TerminalInputBuffer:
|
| 173 |
+
def __init__(self) -> None:
|
| 174 |
+
self._lines: deque[str] = deque()
|
| 175 |
+
self._lock = threading.Lock()
|
| 176 |
+
|
| 177 |
+
def push(self, line: str) -> None:
|
| 178 |
+
with self._lock:
|
| 179 |
+
self._lines.append(line)
|
| 180 |
+
|
| 181 |
+
def drain(self) -> list[str]:
|
| 182 |
+
with self._lock:
|
| 183 |
+
lines = list(self._lines)
|
| 184 |
+
self._lines.clear()
|
| 185 |
+
return lines
|
| 186 |
+
|
| 187 |
+
def prepend_many(self, lines: list[str]) -> None:
|
| 188 |
+
if not lines:
|
| 189 |
+
return
|
| 190 |
+
with self._lock:
|
| 191 |
+
for line in reversed(lines):
|
| 192 |
+
self._lines.appendleft(line)
|
| 193 |
+
|
| 194 |
+
def __len__(self) -> int:
|
| 195 |
+
with self._lock:
|
| 196 |
+
return len(self._lines)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def start_terminal_reader(line_buffer: TerminalInputBuffer, stop_event: threading.Event) -> threading.Thread:
|
| 200 |
+
def read_loop() -> None:
|
| 201 |
+
while not stop_event.is_set():
|
| 202 |
+
line = sys.stdin.readline()
|
| 203 |
+
if line == "":
|
| 204 |
+
stop_event.set()
|
| 205 |
+
break
|
| 206 |
+
line_buffer.push(line.rstrip("\n"))
|
| 207 |
+
|
| 208 |
+
thread = threading.Thread(target=read_loop, name="terminal-input-reader", daemon=True)
|
| 209 |
+
thread.start()
|
| 210 |
+
return thread
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def print_gate_debug(tick_index: int, event_name: str, gate_probability: float) -> None:
|
| 214 |
+
print(f"[tick={tick_index} event={event_name} gate={gate_probability:.6f}]", flush=True)
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
class ThoughtLoopSession:
|
| 218 |
+
def __init__(
|
| 219 |
+
self,
|
| 220 |
+
model: ThoughtLoopT5Gemma,
|
| 221 |
+
tick_seconds: float,
|
| 222 |
+
gate_threshold: float,
|
| 223 |
+
generation_kwargs: dict[str, Any],
|
| 224 |
+
) -> None:
|
| 225 |
+
self.model = model
|
| 226 |
+
self.tokenizer = model.tokenizer
|
| 227 |
+
self.tick_seconds = tick_seconds
|
| 228 |
+
self.gate_threshold = gate_threshold
|
| 229 |
+
self.generation_kwargs = generation_kwargs
|
| 230 |
+
self.z_state = model.initial_state(batch_size=1, device=model.device)
|
| 231 |
+
self.z_mask = model.initial_state_mask(batch_size=1, device=model.device)
|
| 232 |
+
self.use_base_chat_template = should_use_base_chat_template(model.config, self.tokenizer)
|
| 233 |
+
self.elapsed_seconds = 0.0
|
| 234 |
+
self.next_tick_index = 0
|
| 235 |
+
self.feedback_delay_seconds = float(model.config.get("rollout", {}).get("post_speech_feedback_delay_seconds", tick_seconds))
|
| 236 |
+
|
| 237 |
+
def _format_observation(self, observation_role: int, observation_text: str | None) -> str | None:
|
| 238 |
+
if observation_text is None:
|
| 239 |
+
return None
|
| 240 |
+
if observation_role == OBS_ROLE_USER:
|
| 241 |
+
return build_user_generation_observation(
|
| 242 |
+
self.tokenizer,
|
| 243 |
+
observation_text,
|
| 244 |
+
self.use_base_chat_template,
|
| 245 |
+
)
|
| 246 |
+
if observation_role == OBS_ROLE_AGENT_FEEDBACK:
|
| 247 |
+
return build_assistant_feedback_observation(
|
| 248 |
+
self.tokenizer,
|
| 249 |
+
observation_text,
|
| 250 |
+
self.use_base_chat_template,
|
| 251 |
+
)
|
| 252 |
+
return observation_text
|
| 253 |
+
|
| 254 |
+
def _advance_raw(self, delta_seconds: float, observation_role: int, observation_text: str | None) -> float:
|
| 255 |
+
formatted_observation_text = self._format_observation(observation_role, observation_text)
|
| 256 |
+
input_ids, attention_mask = tokenize_text(
|
| 257 |
+
tokenizer=self.tokenizer,
|
| 258 |
+
text=formatted_observation_text,
|
| 259 |
+
max_length=int(self.model.config["model"]["max_observation_tokens"]),
|
| 260 |
+
)
|
| 261 |
+
input_ids_tensor = torch.tensor([input_ids], dtype=torch.long, device=self.model.device)
|
| 262 |
+
attention_mask_tensor = torch.tensor([attention_mask], dtype=torch.long, device=self.model.device)
|
| 263 |
+
role_tensor = torch.tensor([observation_role], dtype=torch.long, device=self.model.device)
|
| 264 |
+
|
| 265 |
+
self.elapsed_seconds += delta_seconds
|
| 266 |
+
|
| 267 |
+
with torch.no_grad():
|
| 268 |
+
self.z_state, gate_logits, self.z_mask = self.model.rollout_step_with_mask(
|
| 269 |
+
z_state=self.z_state,
|
| 270 |
+
state_mask=self.z_mask,
|
| 271 |
+
observation_input_ids=input_ids_tensor,
|
| 272 |
+
observation_attention_mask=attention_mask_tensor,
|
| 273 |
+
observation_role_ids=role_tensor,
|
| 274 |
+
delta_seconds=torch.tensor([delta_seconds], dtype=torch.float32, device=self.model.device),
|
| 275 |
+
elapsed_seconds=torch.tensor([self.elapsed_seconds], dtype=torch.float32, device=self.model.device),
|
| 276 |
+
since_last_user_seconds=torch.zeros(1, dtype=torch.float32, device=self.model.device),
|
| 277 |
+
since_last_agent_seconds=torch.zeros(1, dtype=torch.float32, device=self.model.device),
|
| 278 |
+
)
|
| 279 |
+
return torch.sigmoid(gate_logits).item()
|
| 280 |
+
|
| 281 |
+
def _advance_tick(self, observation_role: int, observation_text: str | None) -> float:
|
| 282 |
+
delta_seconds = 0.0 if self.next_tick_index == 0 else self.tick_seconds
|
| 283 |
+
gate_probability = self._advance_raw(delta_seconds, observation_role, observation_text)
|
| 284 |
+
self.next_tick_index += 1
|
| 285 |
+
return gate_probability
|
| 286 |
+
|
| 287 |
+
def advance_tick(self, observation_role: int, observation_text: str | None) -> float:
|
| 288 |
+
return self._advance_tick(observation_role, observation_text)
|
| 289 |
+
|
| 290 |
+
def replay_history(self, messages: list[dict[str, Any]]) -> None:
|
| 291 |
+
quantized_events = quantize_message_sequence(
|
| 292 |
+
messages=messages,
|
| 293 |
+
tick_seconds=self.tick_seconds,
|
| 294 |
+
feedback_delay_seconds=self.feedback_delay_seconds,
|
| 295 |
+
)
|
| 296 |
+
events_by_tick = {int(event["tick"]): event for event in quantized_events}
|
| 297 |
+
if not events_by_tick:
|
| 298 |
+
return
|
| 299 |
+
|
| 300 |
+
last_tick = max(events_by_tick)
|
| 301 |
+
while self.next_tick_index <= last_tick:
|
| 302 |
+
event = events_by_tick.get(self.next_tick_index)
|
| 303 |
+
if event is None:
|
| 304 |
+
self._advance_tick(OBS_ROLE_NONE, None)
|
| 305 |
+
continue
|
| 306 |
+
|
| 307 |
+
if event["kind"] == "user":
|
| 308 |
+
self._advance_tick(OBS_ROLE_USER, str(event["text"]))
|
| 309 |
+
elif event["kind"] == "agent_feedback":
|
| 310 |
+
self._advance_tick(OBS_ROLE_AGENT_FEEDBACK, str(event["text"]))
|
| 311 |
+
else:
|
| 312 |
+
self._advance_tick(OBS_ROLE_NONE, None)
|
| 313 |
+
|
| 314 |
+
def _generate_response(self) -> str:
|
| 315 |
+
generated = self.model.generate_from_state(
|
| 316 |
+
self.z_state,
|
| 317 |
+
encoder_attention_mask=self.z_mask,
|
| 318 |
+
**self.generation_kwargs,
|
| 319 |
+
)
|
| 320 |
+
return self.tokenizer.decode(generated[0], skip_special_tokens=True).strip()
|
| 321 |
+
|
| 322 |
+
def generate_response(self) -> str:
|
| 323 |
+
return self._generate_response()
|
| 324 |
+
|
| 325 |
+
def respond(
|
| 326 |
+
self,
|
| 327 |
+
user_text: str,
|
| 328 |
+
*,
|
| 329 |
+
max_thought_seconds: float,
|
| 330 |
+
settle_thought_seconds: float,
|
| 331 |
+
max_autonomous_utterances: int,
|
| 332 |
+
debug_gate: bool = False,
|
| 333 |
+
) -> tuple[list[str], float]:
|
| 334 |
+
gate_probability = self._advance_tick(OBS_ROLE_USER, user_text)
|
| 335 |
+
if debug_gate:
|
| 336 |
+
print_gate_debug(self.next_tick_index - 1, "user", gate_probability)
|
| 337 |
+
utterances: list[str] = []
|
| 338 |
+
thought_ticks = 0
|
| 339 |
+
settle_ticks = max(1, int(math.ceil(settle_thought_seconds / self.tick_seconds)))
|
| 340 |
+
max_thought_ticks = max(1, int(math.ceil(max_thought_seconds / self.tick_seconds)))
|
| 341 |
+
idle_ticks_after_last_speech = 0
|
| 342 |
+
pending_assistant_feedback: str | None = None
|
| 343 |
+
|
| 344 |
+
while thought_ticks < max_thought_ticks:
|
| 345 |
+
if pending_assistant_feedback is not None:
|
| 346 |
+
event_name = "assistant_feedback"
|
| 347 |
+
gate_probability = self._advance_tick(OBS_ROLE_AGENT_FEEDBACK, pending_assistant_feedback)
|
| 348 |
+
pending_assistant_feedback = None
|
| 349 |
+
else:
|
| 350 |
+
event_name = "idle"
|
| 351 |
+
gate_probability = self._advance_tick(OBS_ROLE_NONE, None)
|
| 352 |
+
if debug_gate:
|
| 353 |
+
print_gate_debug(self.next_tick_index - 1, event_name, gate_probability)
|
| 354 |
+
thought_ticks += 1
|
| 355 |
+
idle_ticks_after_last_speech += 1
|
| 356 |
+
|
| 357 |
+
if gate_probability >= self.gate_threshold:
|
| 358 |
+
response = self._generate_response()
|
| 359 |
+
utterances.append(response)
|
| 360 |
+
idle_ticks_after_last_speech = 0
|
| 361 |
+
if len(utterances) >= max_autonomous_utterances:
|
| 362 |
+
break
|
| 363 |
+
pending_assistant_feedback = response
|
| 364 |
+
continue
|
| 365 |
+
|
| 366 |
+
if utterances and idle_ticks_after_last_speech >= settle_ticks:
|
| 367 |
+
break
|
| 368 |
+
|
| 369 |
+
return utterances, thought_ticks * self.tick_seconds
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def consume_next_live_user_line(
|
| 373 |
+
line_buffer: TerminalInputBuffer,
|
| 374 |
+
*,
|
| 375 |
+
stop_event: threading.Event,
|
| 376 |
+
status_text: str,
|
| 377 |
+
) -> str | None:
|
| 378 |
+
lines = line_buffer.drain()
|
| 379 |
+
if not lines:
|
| 380 |
+
return None
|
| 381 |
+
|
| 382 |
+
selected_line: str | None = None
|
| 383 |
+
deferred_lines: list[str] = []
|
| 384 |
+
for raw_line in lines:
|
| 385 |
+
line = raw_line.strip()
|
| 386 |
+
if not line:
|
| 387 |
+
continue
|
| 388 |
+
command = line.lower()
|
| 389 |
+
if command in {"/exit", "/quit", "exit", "quit"}:
|
| 390 |
+
stop_event.set()
|
| 391 |
+
continue
|
| 392 |
+
if command == "/status":
|
| 393 |
+
print(status_text, flush=True)
|
| 394 |
+
continue
|
| 395 |
+
if selected_line is None:
|
| 396 |
+
selected_line = line
|
| 397 |
+
else:
|
| 398 |
+
deferred_lines.append(line)
|
| 399 |
+
|
| 400 |
+
line_buffer.prepend_many(deferred_lines)
|
| 401 |
+
return selected_line
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
def run_live_thought_loop_terminal(
|
| 405 |
+
session: ThoughtLoopSession,
|
| 406 |
+
*,
|
| 407 |
+
gate_threshold: float,
|
| 408 |
+
debug_gate: bool,
|
| 409 |
+
) -> None:
|
| 410 |
+
line_buffer = TerminalInputBuffer()
|
| 411 |
+
stop_event = threading.Event()
|
| 412 |
+
start_terminal_reader(line_buffer, stop_event)
|
| 413 |
+
pending_assistant_feedback: str | None = None
|
| 414 |
+
|
| 415 |
+
print("Live Samantha session.", flush=True)
|
| 416 |
+
print("Type anytime and press Enter. Each line is one user event on the next 1 Hz tick.", flush=True)
|
| 417 |
+
print("Commands: /status, /exit", flush=True)
|
| 418 |
+
|
| 419 |
+
while not stop_event.is_set():
|
| 420 |
+
tick_started_at = time.monotonic()
|
| 421 |
+
status_text = (
|
| 422 |
+
f"[status] tick={session.next_tick_index} elapsed={session.elapsed_seconds:.1f}s "
|
| 423 |
+
f"queued_user_lines={len(line_buffer)} pending_assistant_feedback={pending_assistant_feedback is not None}"
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
if pending_assistant_feedback is not None:
|
| 427 |
+
observation_role = OBS_ROLE_AGENT_FEEDBACK
|
| 428 |
+
observation_text = pending_assistant_feedback
|
| 429 |
+
event_name = "assistant_feedback"
|
| 430 |
+
pending_assistant_feedback = None
|
| 431 |
+
else:
|
| 432 |
+
user_line = consume_next_live_user_line(
|
| 433 |
+
line_buffer,
|
| 434 |
+
stop_event=stop_event,
|
| 435 |
+
status_text=status_text,
|
| 436 |
+
)
|
| 437 |
+
if stop_event.is_set():
|
| 438 |
+
break
|
| 439 |
+
|
| 440 |
+
if user_line is not None:
|
| 441 |
+
observation_role = OBS_ROLE_USER
|
| 442 |
+
observation_text = user_line
|
| 443 |
+
event_name = "user"
|
| 444 |
+
print(f"user> {user_line}", flush=True)
|
| 445 |
+
else:
|
| 446 |
+
observation_role = OBS_ROLE_NONE
|
| 447 |
+
observation_text = None
|
| 448 |
+
event_name = "idle"
|
| 449 |
+
|
| 450 |
+
gate_probability = session.advance_tick(observation_role, observation_text)
|
| 451 |
+
if debug_gate:
|
| 452 |
+
print_gate_debug(session.next_tick_index - 1, event_name, gate_probability)
|
| 453 |
+
|
| 454 |
+
if observation_role != OBS_ROLE_USER and gate_probability >= gate_threshold:
|
| 455 |
+
response = session.generate_response()
|
| 456 |
+
print(f"assistant> {response}", flush=True)
|
| 457 |
+
pending_assistant_feedback = response
|
| 458 |
+
|
| 459 |
+
sleep_seconds = session.tick_seconds - (time.monotonic() - tick_started_at)
|
| 460 |
+
if sleep_seconds > 0.0:
|
| 461 |
+
time.sleep(sleep_seconds)
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
def run_blocking_base_terminal(session: BaseSeq2SeqSession) -> None:
|
| 465 |
+
print("Raw HF baseline terminal mode. This model has no Samantha Z state or speech gate.", flush=True)
|
| 466 |
+
print("Type a message and press Enter. Commands: /exit", flush=True)
|
| 467 |
+
while True:
|
| 468 |
+
user_text = input("user> ").strip()
|
| 469 |
+
if user_text.lower() in {"/exit", "/quit", "exit", "quit"}:
|
| 470 |
+
break
|
| 471 |
+
if not user_text:
|
| 472 |
+
continue
|
| 473 |
+
responses, _ = session.respond(
|
| 474 |
+
user_text,
|
| 475 |
+
max_thought_seconds=0.0,
|
| 476 |
+
settle_thought_seconds=0.0,
|
| 477 |
+
max_autonomous_utterances=1,
|
| 478 |
+
)
|
| 479 |
+
for response in responses:
|
| 480 |
+
print(f"assistant> {response}", flush=True)
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
def main() -> None:
|
| 484 |
+
args = parse_args()
|
| 485 |
+
backend_kind, model, tokenizer = load_inference_backend(args.model_path, device=args.device)
|
| 486 |
+
generation_kwargs = build_generation_kwargs(args)
|
| 487 |
+
|
| 488 |
+
if backend_kind == "thought_loop":
|
| 489 |
+
tick_seconds = (
|
| 490 |
+
float(args.tick_seconds)
|
| 491 |
+
if args.tick_seconds is not None
|
| 492 |
+
else float(model.config.get("rollout", {}).get("tick_seconds", 1.0))
|
| 493 |
+
)
|
| 494 |
+
session: Any = ThoughtLoopSession(
|
| 495 |
+
model=model,
|
| 496 |
+
tick_seconds=tick_seconds,
|
| 497 |
+
gate_threshold=args.gate_threshold,
|
| 498 |
+
generation_kwargs=generation_kwargs,
|
| 499 |
+
)
|
| 500 |
+
else:
|
| 501 |
+
assert tokenizer is not None
|
| 502 |
+
session = BaseSeq2SeqSession(
|
| 503 |
+
model=model,
|
| 504 |
+
tokenizer=tokenizer,
|
| 505 |
+
device=args.device,
|
| 506 |
+
generation_kwargs=generation_kwargs,
|
| 507 |
+
)
|
| 508 |
+
|
| 509 |
+
if args.history_json:
|
| 510 |
+
history = json.loads(Path(args.history_json).read_text(encoding="utf-8"))
|
| 511 |
+
if not isinstance(history, list):
|
| 512 |
+
raise ValueError("History JSON must be a list of turn objects.")
|
| 513 |
+
session.replay_history(history)
|
| 514 |
+
|
| 515 |
+
if args.user:
|
| 516 |
+
responses, thought_time = session.respond(
|
| 517 |
+
args.user,
|
| 518 |
+
max_thought_seconds=args.max_thought_seconds,
|
| 519 |
+
settle_thought_seconds=args.settle_thought_seconds,
|
| 520 |
+
max_autonomous_utterances=args.max_autonomous_utterances,
|
| 521 |
+
debug_gate=bool(args.debug_gate),
|
| 522 |
+
)
|
| 523 |
+
print(
|
| 524 |
+
json.dumps(
|
| 525 |
+
{
|
| 526 |
+
"responses": responses,
|
| 527 |
+
"thought_seconds": thought_time,
|
| 528 |
+
},
|
| 529 |
+
indent=2,
|
| 530 |
+
ensure_ascii=False,
|
| 531 |
+
)
|
| 532 |
+
)
|
| 533 |
+
|
| 534 |
+
if args.interactive or args.user is None:
|
| 535 |
+
if backend_kind == "thought_loop":
|
| 536 |
+
run_live_thought_loop_terminal(
|
| 537 |
+
session,
|
| 538 |
+
gate_threshold=float(args.gate_threshold),
|
| 539 |
+
debug_gate=bool(args.debug_gate),
|
| 540 |
+
)
|
| 541 |
+
else:
|
| 542 |
+
run_blocking_base_terminal(session)
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
if __name__ == "__main__":
|
| 546 |
+
main()
|
initial_latent_z.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9c8a7ba9282608a91981002cf45998c493d47ba4ccb5538fae1f183e9659d344
|
| 3 |
+
size 1048656
|
model.py
ADDED
|
@@ -0,0 +1,777 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import math
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
import torch.nn as nn
|
| 11 |
+
import torch.nn.functional as F
|
| 12 |
+
from huggingface_hub import snapshot_download
|
| 13 |
+
from safetensors.torch import load_model, save_file, save_model
|
| 14 |
+
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
|
| 15 |
+
from transformers.modeling_outputs import BaseModelOutput
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
| 19 |
+
if str(SCRIPT_DIR) not in sys.path:
|
| 20 |
+
sys.path.insert(0, str(SCRIPT_DIR))
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class ParameterlessRMSNorm(nn.Module):
|
| 24 |
+
def __init__(self, hidden_size: int, eps: float = 1e-6) -> None:
|
| 25 |
+
super().__init__()
|
| 26 |
+
self.hidden_size = hidden_size
|
| 27 |
+
self.eps = eps
|
| 28 |
+
|
| 29 |
+
def forward(self, inputs: torch.Tensor) -> torch.Tensor:
|
| 30 |
+
original_dtype = inputs.dtype
|
| 31 |
+
normalized = inputs.float()
|
| 32 |
+
normalized = normalized * torch.rsqrt(normalized.pow(2).mean(dim=-1, keepdim=True) + self.eps)
|
| 33 |
+
return normalized.to(dtype=original_dtype)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class TemporalFeatureProjector(nn.Module):
|
| 37 |
+
def __init__(self, hidden_size: int, num_time_tokens: int, scalar_count: int = 4) -> None:
|
| 38 |
+
super().__init__()
|
| 39 |
+
self.hidden_size = hidden_size
|
| 40 |
+
self.num_time_tokens = num_time_tokens
|
| 41 |
+
self.mlp = nn.Sequential(
|
| 42 |
+
nn.Linear(scalar_count, hidden_size),
|
| 43 |
+
nn.GELU(),
|
| 44 |
+
nn.Linear(hidden_size, hidden_size * num_time_tokens),
|
| 45 |
+
)
|
| 46 |
+
self.norm = nn.LayerNorm(hidden_size)
|
| 47 |
+
|
| 48 |
+
def forward(self, temporal_features: torch.Tensor) -> torch.Tensor:
|
| 49 |
+
projected = self.mlp(temporal_features)
|
| 50 |
+
projected = projected.view(temporal_features.size(0), self.num_time_tokens, self.hidden_size)
|
| 51 |
+
return self.norm(projected)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class ThoughtLoopT5Gemma(nn.Module):
|
| 55 |
+
def __init__(self, config: dict[str, Any]) -> None:
|
| 56 |
+
super().__init__()
|
| 57 |
+
self.config = config
|
| 58 |
+
model_cfg = config["model"]
|
| 59 |
+
training_cfg = config.get("training", {})
|
| 60 |
+
|
| 61 |
+
backbone_dtype = getattr(torch, model_cfg.get("dtype", "bfloat16"))
|
| 62 |
+
backbone_kwargs: dict[str, Any] = {
|
| 63 |
+
# transformers currently warns that torch_dtype is deprecated in favor of dtype,
|
| 64 |
+
# but torch_dtype remains compatible across more installed versions.
|
| 65 |
+
"torch_dtype": backbone_dtype,
|
| 66 |
+
}
|
| 67 |
+
attn_implementation = model_cfg.get("attn_implementation")
|
| 68 |
+
if attn_implementation:
|
| 69 |
+
backbone_kwargs["attn_implementation"] = attn_implementation
|
| 70 |
+
|
| 71 |
+
self.backbone = AutoModelForSeq2SeqLM.from_pretrained(model_cfg["base_model_name"], **backbone_kwargs)
|
| 72 |
+
self.tokenizer = AutoTokenizer.from_pretrained(model_cfg["base_model_name"])
|
| 73 |
+
|
| 74 |
+
if training_cfg.get("gradient_checkpointing", True):
|
| 75 |
+
self.backbone.gradient_checkpointing_enable()
|
| 76 |
+
if getattr(self.backbone.config, "use_cache", None) is not None:
|
| 77 |
+
self.backbone.config.use_cache = False
|
| 78 |
+
|
| 79 |
+
self.encoder = self.backbone.get_encoder()
|
| 80 |
+
self.decoder = self.backbone.get_decoder()
|
| 81 |
+
self.input_embeddings = self.backbone.get_input_embeddings()
|
| 82 |
+
hidden_size = int(self.backbone.config.decoder.hidden_size)
|
| 83 |
+
self.hidden_size = hidden_size
|
| 84 |
+
self.z_slots = int(model_cfg["z_slots"])
|
| 85 |
+
self.thought_loop_proposal_mode = str(
|
| 86 |
+
model_cfg.get("thought_loop_proposal_mode", "latent_prefix")
|
| 87 |
+
).strip().lower()
|
| 88 |
+
if self.thought_loop_proposal_mode not in {"latent_prefix", "observation_hidden_compression"}:
|
| 89 |
+
raise ValueError(f"Unsupported thought_loop_proposal_mode: {self.thought_loop_proposal_mode}")
|
| 90 |
+
self.preserve_observation_encoder_manifold = bool(
|
| 91 |
+
model_cfg.get(
|
| 92 |
+
"preserve_observation_encoder_manifold",
|
| 93 |
+
self.thought_loop_proposal_mode == "observation_hidden_compression",
|
| 94 |
+
)
|
| 95 |
+
)
|
| 96 |
+
self.observation_encoder_use_state_context = bool(
|
| 97 |
+
model_cfg.get("observation_encoder_use_state_context", False)
|
| 98 |
+
)
|
| 99 |
+
self.latent_attention_mask_mode = str(
|
| 100 |
+
model_cfg.get("latent_attention_mask_mode", "observed")
|
| 101 |
+
).strip().lower()
|
| 102 |
+
if self.latent_attention_mask_mode not in {"observed", "full"}:
|
| 103 |
+
raise ValueError(f"Unsupported latent_attention_mask_mode: {self.latent_attention_mask_mode}")
|
| 104 |
+
self.use_explicit_time_features = bool(model_cfg.get("use_explicit_time_features", False))
|
| 105 |
+
self.num_time_tokens = int(model_cfg["num_time_tokens"]) if self.use_explicit_time_features else 0
|
| 106 |
+
self.observation_role_count = 3
|
| 107 |
+
magicnorm_eps = float(model_cfg.get("magicnorm_eps", 1e-6))
|
| 108 |
+
|
| 109 |
+
# Keep the newly initialized recurrent/gating modules in fp32. We cast only the
|
| 110 |
+
# tensors handed into the bf16 backbone. This avoids dtype crashes while preserving
|
| 111 |
+
# stable optimizer state for the custom modules.
|
| 112 |
+
self.z_init = nn.Parameter(torch.randn(self.z_slots, hidden_size) * 0.02)
|
| 113 |
+
self.segment_embeddings = nn.Embedding(3, hidden_size)
|
| 114 |
+
self.observation_role_embeddings = nn.Embedding(self.observation_role_count, hidden_size)
|
| 115 |
+
self.temporal_projector = (
|
| 116 |
+
TemporalFeatureProjector(hidden_size, self.num_time_tokens) if self.use_explicit_time_features else None
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
self.state_gate = nn.Sequential(
|
| 120 |
+
nn.Linear(hidden_size * 2, hidden_size),
|
| 121 |
+
nn.GELU(),
|
| 122 |
+
nn.Linear(hidden_size, hidden_size),
|
| 123 |
+
)
|
| 124 |
+
self._initialize_update_gate_bias(model_cfg)
|
| 125 |
+
self.state_gate_input_norm = ParameterlessRMSNorm(hidden_size * 2, eps=magicnorm_eps)
|
| 126 |
+
self.proposed_state_norm = ParameterlessRMSNorm(hidden_size, eps=magicnorm_eps)
|
| 127 |
+
self.recurrent_state_norm = ParameterlessRMSNorm(hidden_size, eps=magicnorm_eps)
|
| 128 |
+
self.gate_context_norm = ParameterlessRMSNorm(hidden_size, eps=magicnorm_eps)
|
| 129 |
+
|
| 130 |
+
self.gate_query = nn.Parameter(torch.randn(1, 1, hidden_size) * 0.02)
|
| 131 |
+
self.gate_pool = nn.MultiheadAttention(
|
| 132 |
+
embed_dim=hidden_size,
|
| 133 |
+
num_heads=int(model_cfg.get("gate_attention_heads", 4)),
|
| 134 |
+
batch_first=True,
|
| 135 |
+
)
|
| 136 |
+
self.gate_head = nn.Sequential(
|
| 137 |
+
nn.LayerNorm(hidden_size),
|
| 138 |
+
nn.Linear(hidden_size, hidden_size),
|
| 139 |
+
nn.GELU(),
|
| 140 |
+
nn.Linear(hidden_size, 1),
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
self._drop_unused_vision_modules()
|
| 144 |
+
|
| 145 |
+
def _initialize_update_gate_bias(self, model_cfg: dict[str, Any]) -> None:
|
| 146 |
+
if "initial_update_gate_bias" not in model_cfg:
|
| 147 |
+
return
|
| 148 |
+
final_gate_layer = self.state_gate[-1]
|
| 149 |
+
if not isinstance(final_gate_layer, nn.Linear) or final_gate_layer.bias is None:
|
| 150 |
+
raise RuntimeError("state_gate must end in a Linear layer with bias to use initial_update_gate_bias.")
|
| 151 |
+
nn.init.constant_(final_gate_layer.bias, float(model_cfg["initial_update_gate_bias"]))
|
| 152 |
+
|
| 153 |
+
def _drop_unused_vision_modules(self) -> None:
|
| 154 |
+
for module_path in (
|
| 155 |
+
"encoder.vision_tower",
|
| 156 |
+
"encoder.vision_model",
|
| 157 |
+
"model.encoder.vision_tower",
|
| 158 |
+
"model.encoder.vision_model",
|
| 159 |
+
):
|
| 160 |
+
parent: Any = self.backbone
|
| 161 |
+
path_parts = module_path.split(".")
|
| 162 |
+
try:
|
| 163 |
+
for part in path_parts[:-1]:
|
| 164 |
+
parent = getattr(parent, part)
|
| 165 |
+
except AttributeError:
|
| 166 |
+
continue
|
| 167 |
+
|
| 168 |
+
child_name = path_parts[-1]
|
| 169 |
+
child = getattr(parent, child_name, None)
|
| 170 |
+
if isinstance(child, nn.Module):
|
| 171 |
+
setattr(parent, child_name, None)
|
| 172 |
+
|
| 173 |
+
@property
|
| 174 |
+
def device(self) -> torch.device:
|
| 175 |
+
return next(self.parameters()).device
|
| 176 |
+
|
| 177 |
+
@property
|
| 178 |
+
def backbone_dtype(self) -> torch.dtype:
|
| 179 |
+
return self.input_embeddings.weight.dtype
|
| 180 |
+
|
| 181 |
+
@property
|
| 182 |
+
def recurrent_dtype(self) -> torch.dtype:
|
| 183 |
+
return self.z_init.dtype
|
| 184 |
+
|
| 185 |
+
def trainable_parameter_count(self) -> int:
|
| 186 |
+
return sum(parameter.numel() for parameter in self.parameters() if parameter.requires_grad)
|
| 187 |
+
|
| 188 |
+
def _decoder_blocks(self) -> Any:
|
| 189 |
+
for attribute_name in ("block", "layers", "h"):
|
| 190 |
+
blocks = getattr(self.decoder, attribute_name, None)
|
| 191 |
+
if blocks is not None:
|
| 192 |
+
return blocks
|
| 193 |
+
return None
|
| 194 |
+
|
| 195 |
+
def decoder_layer_count(self) -> int:
|
| 196 |
+
blocks = self._decoder_blocks()
|
| 197 |
+
return len(blocks) if blocks is not None else 0
|
| 198 |
+
|
| 199 |
+
def set_gate_trainable(self, trainable: bool) -> None:
|
| 200 |
+
self.gate_query.requires_grad_(trainable)
|
| 201 |
+
self.gate_pool.requires_grad_(trainable)
|
| 202 |
+
self.gate_head.requires_grad_(trainable)
|
| 203 |
+
self.gate_context_norm.requires_grad_(trainable)
|
| 204 |
+
|
| 205 |
+
def set_decoder_trainable_fraction(self, trainable_fraction: float) -> int:
|
| 206 |
+
blocks = self._decoder_blocks()
|
| 207 |
+
if blocks is None:
|
| 208 |
+
return 0
|
| 209 |
+
|
| 210 |
+
clamped_fraction = min(max(trainable_fraction, 0.0), 1.0)
|
| 211 |
+
total_blocks = len(blocks)
|
| 212 |
+
trainable_blocks = min(total_blocks, math.ceil(total_blocks * clamped_fraction)) if clamped_fraction > 0 else 0
|
| 213 |
+
first_trainable_index = total_blocks - trainable_blocks
|
| 214 |
+
|
| 215 |
+
for block_index, block in enumerate(blocks):
|
| 216 |
+
block.requires_grad_(block_index >= first_trainable_index)
|
| 217 |
+
|
| 218 |
+
for attribute_name in ("final_layer_norm", "norm", "layer_norm"):
|
| 219 |
+
maybe_module = getattr(self.decoder, attribute_name, None)
|
| 220 |
+
if isinstance(maybe_module, nn.Module):
|
| 221 |
+
maybe_module.requires_grad_(trainable_blocks > 0)
|
| 222 |
+
|
| 223 |
+
decoder_embeddings = getattr(self.decoder, "embed_tokens", None)
|
| 224 |
+
if isinstance(decoder_embeddings, nn.Module):
|
| 225 |
+
decoder_embeddings.requires_grad_(trainable_blocks > 0)
|
| 226 |
+
|
| 227 |
+
for module_path in (
|
| 228 |
+
"lm_head",
|
| 229 |
+
"model.lm_head",
|
| 230 |
+
"backbone.lm_head",
|
| 231 |
+
"shared",
|
| 232 |
+
"model.shared",
|
| 233 |
+
):
|
| 234 |
+
parent: Any = self
|
| 235 |
+
path_parts = module_path.split(".")
|
| 236 |
+
try:
|
| 237 |
+
for part in path_parts[:-1]:
|
| 238 |
+
parent = getattr(parent, part)
|
| 239 |
+
except AttributeError:
|
| 240 |
+
continue
|
| 241 |
+
|
| 242 |
+
child_name = path_parts[-1]
|
| 243 |
+
child = getattr(parent, child_name, None)
|
| 244 |
+
if isinstance(child, nn.Module):
|
| 245 |
+
child.requires_grad_(trainable_blocks > 0)
|
| 246 |
+
|
| 247 |
+
return trainable_blocks
|
| 248 |
+
|
| 249 |
+
def initial_state(self, batch_size: int, device: torch.device | None = None) -> torch.Tensor:
|
| 250 |
+
target_device = device or self.device
|
| 251 |
+
initial_state = self.z_init.unsqueeze(0).expand(batch_size, -1, -1).to(device=target_device)
|
| 252 |
+
return self.recurrent_state_norm(initial_state)
|
| 253 |
+
|
| 254 |
+
def initial_state_mask(self, batch_size: int, device: torch.device | None = None) -> torch.Tensor:
|
| 255 |
+
target_device = device or self.device
|
| 256 |
+
if self.latent_attention_mask_mode == "full":
|
| 257 |
+
return torch.ones(batch_size, self.z_slots, dtype=torch.long, device=target_device)
|
| 258 |
+
if self.thought_loop_proposal_mode == "observation_hidden_compression":
|
| 259 |
+
return torch.zeros(batch_size, self.z_slots, dtype=torch.long, device=target_device)
|
| 260 |
+
return torch.ones(batch_size, self.z_slots, dtype=torch.long, device=target_device)
|
| 261 |
+
|
| 262 |
+
def _build_temporal_tensor(
|
| 263 |
+
self,
|
| 264 |
+
delta_seconds: torch.Tensor,
|
| 265 |
+
elapsed_seconds: torch.Tensor,
|
| 266 |
+
since_last_user_seconds: torch.Tensor,
|
| 267 |
+
since_last_agent_seconds: torch.Tensor,
|
| 268 |
+
) -> torch.Tensor:
|
| 269 |
+
if not self.use_explicit_time_features:
|
| 270 |
+
raise RuntimeError("Temporal features were requested, but this model is configured to disable them.")
|
| 271 |
+
stacked = torch.stack(
|
| 272 |
+
[
|
| 273 |
+
torch.log1p(delta_seconds),
|
| 274 |
+
torch.log1p(elapsed_seconds),
|
| 275 |
+
torch.log1p(since_last_user_seconds),
|
| 276 |
+
torch.log1p(since_last_agent_seconds),
|
| 277 |
+
],
|
| 278 |
+
dim=-1,
|
| 279 |
+
)
|
| 280 |
+
return stacked
|
| 281 |
+
|
| 282 |
+
def compress_hidden_states_to_slots(
|
| 283 |
+
self,
|
| 284 |
+
hidden_states: torch.Tensor,
|
| 285 |
+
attention_mask: torch.Tensor,
|
| 286 |
+
target_slots: int | None = None,
|
| 287 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 288 |
+
slots = int(target_slots if target_slots is not None else self.z_slots)
|
| 289 |
+
batch_size, _, hidden_size = hidden_states.shape
|
| 290 |
+
compressed_hidden = hidden_states.new_zeros((batch_size, slots, hidden_size))
|
| 291 |
+
compressed_mask = torch.zeros(batch_size, slots, dtype=torch.long, device=hidden_states.device)
|
| 292 |
+
attention_mask = attention_mask.to(device=hidden_states.device, dtype=torch.bool)
|
| 293 |
+
|
| 294 |
+
for batch_index in range(batch_size):
|
| 295 |
+
valid_states = hidden_states[batch_index, attention_mask[batch_index]]
|
| 296 |
+
valid_length = int(valid_states.shape[0])
|
| 297 |
+
if valid_length <= 0:
|
| 298 |
+
continue
|
| 299 |
+
|
| 300 |
+
if valid_length <= slots:
|
| 301 |
+
compressed_hidden[batch_index, :valid_length] = valid_states
|
| 302 |
+
compressed_mask[batch_index, :valid_length] = 1
|
| 303 |
+
continue
|
| 304 |
+
|
| 305 |
+
for slot_index in range(slots):
|
| 306 |
+
start = (slot_index * valid_length) // slots
|
| 307 |
+
end = ((slot_index + 1) * valid_length) // slots
|
| 308 |
+
if end <= start:
|
| 309 |
+
end = min(valid_length, start + 1)
|
| 310 |
+
compressed_hidden[batch_index, slot_index] = valid_states[start:end].mean(dim=0)
|
| 311 |
+
compressed_mask[batch_index, slot_index] = 1
|
| 312 |
+
|
| 313 |
+
return compressed_hidden, compressed_mask
|
| 314 |
+
|
| 315 |
+
def _rollout_step_impl(
|
| 316 |
+
self,
|
| 317 |
+
z_state: torch.Tensor,
|
| 318 |
+
observation_input_ids: torch.Tensor,
|
| 319 |
+
observation_attention_mask: torch.Tensor,
|
| 320 |
+
observation_role_ids: torch.Tensor,
|
| 321 |
+
delta_seconds: torch.Tensor,
|
| 322 |
+
elapsed_seconds: torch.Tensor,
|
| 323 |
+
since_last_user_seconds: torch.Tensor,
|
| 324 |
+
since_last_agent_seconds: torch.Tensor,
|
| 325 |
+
previous_state_mask: torch.Tensor | None = None,
|
| 326 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 327 |
+
batch_size = z_state.size(0)
|
| 328 |
+
recurrent_dtype = z_state.dtype
|
| 329 |
+
backbone_dtype = self.backbone_dtype
|
| 330 |
+
|
| 331 |
+
if self.thought_loop_proposal_mode == "latent_prefix":
|
| 332 |
+
aux_tokens: list[torch.Tensor] = []
|
| 333 |
+
aux_masks: list[torch.Tensor] = []
|
| 334 |
+
if self.use_explicit_time_features:
|
| 335 |
+
temporal_features = self._build_temporal_tensor(
|
| 336 |
+
delta_seconds=delta_seconds,
|
| 337 |
+
elapsed_seconds=elapsed_seconds,
|
| 338 |
+
since_last_user_seconds=since_last_user_seconds,
|
| 339 |
+
since_last_agent_seconds=since_last_agent_seconds,
|
| 340 |
+
).to(dtype=recurrent_dtype)
|
| 341 |
+
assert self.temporal_projector is not None
|
| 342 |
+
time_tokens = self.temporal_projector(temporal_features)
|
| 343 |
+
time_tokens = time_tokens + self.segment_embeddings.weight[1].view(1, 1, -1).to(dtype=recurrent_dtype)
|
| 344 |
+
aux_tokens.append(time_tokens)
|
| 345 |
+
aux_masks.append(torch.ones(batch_size, self.num_time_tokens, device=z_state.device, dtype=torch.long))
|
| 346 |
+
|
| 347 |
+
role_tokens = self.observation_role_embeddings(observation_role_ids).unsqueeze(1).to(dtype=recurrent_dtype)
|
| 348 |
+
role_tokens = role_tokens + self.segment_embeddings.weight[1].view(1, 1, -1).to(dtype=recurrent_dtype)
|
| 349 |
+
aux_tokens.append(role_tokens)
|
| 350 |
+
aux_masks.append(torch.ones(batch_size, 1, device=z_state.device, dtype=torch.long))
|
| 351 |
+
|
| 352 |
+
observation_embeds = self.input_embeddings(observation_input_ids).to(dtype=recurrent_dtype)
|
| 353 |
+
observation_embeds = observation_embeds + self.segment_embeddings.weight[2].view(1, 1, -1).to(
|
| 354 |
+
dtype=recurrent_dtype
|
| 355 |
+
)
|
| 356 |
+
observation_embeds = observation_embeds + self.observation_role_embeddings(observation_role_ids).unsqueeze(1).to(
|
| 357 |
+
dtype=recurrent_dtype
|
| 358 |
+
)
|
| 359 |
+
|
| 360 |
+
z_tokens = z_state + self.segment_embeddings.weight[0].view(1, 1, -1).to(dtype=recurrent_dtype)
|
| 361 |
+
encoder_inputs = torch.cat([z_tokens, *aux_tokens, observation_embeds], dim=1)
|
| 362 |
+
|
| 363 |
+
z_mask = torch.ones(batch_size, self.z_slots, device=z_state.device, dtype=torch.long)
|
| 364 |
+
encoder_attention_mask = torch.cat([z_mask, *aux_masks, observation_attention_mask.long()], dim=1)
|
| 365 |
+
|
| 366 |
+
# The pretrained T5Gemma backbone was loaded in bf16. Passing fp32 inputs_embeds
|
| 367 |
+
# into its bf16 Linear layers causes: expected mat1 and mat2 to have same dtype.
|
| 368 |
+
encoder_outputs = self.encoder(
|
| 369 |
+
inputs_embeds=encoder_inputs.to(dtype=backbone_dtype),
|
| 370 |
+
attention_mask=encoder_attention_mask,
|
| 371 |
+
return_dict=True,
|
| 372 |
+
)
|
| 373 |
+
latent_prefix_state = encoder_outputs.last_hidden_state[:, : self.z_slots, :].to(dtype=recurrent_dtype)
|
| 374 |
+
proposed_state = latent_prefix_state
|
| 375 |
+
proposal_mask = torch.ones(batch_size, self.z_slots, dtype=torch.long, device=z_state.device)
|
| 376 |
+
else:
|
| 377 |
+
use_state_context = (
|
| 378 |
+
self.observation_encoder_use_state_context
|
| 379 |
+
and previous_state_mask is not None
|
| 380 |
+
and torch.any(previous_state_mask > 0)
|
| 381 |
+
)
|
| 382 |
+
if use_state_context:
|
| 383 |
+
observation_embeds = self.input_embeddings(observation_input_ids).to(dtype=recurrent_dtype)
|
| 384 |
+
encoder_inputs = torch.cat([z_state, observation_embeds], dim=1)
|
| 385 |
+
encoder_attention_mask = torch.cat(
|
| 386 |
+
[
|
| 387 |
+
previous_state_mask.to(device=z_state.device, dtype=torch.long),
|
| 388 |
+
observation_attention_mask.long(),
|
| 389 |
+
],
|
| 390 |
+
dim=1,
|
| 391 |
+
)
|
| 392 |
+
observation_encoder_outputs = self.encoder(
|
| 393 |
+
inputs_embeds=encoder_inputs.to(dtype=backbone_dtype),
|
| 394 |
+
attention_mask=encoder_attention_mask,
|
| 395 |
+
return_dict=True,
|
| 396 |
+
)
|
| 397 |
+
if self.latent_attention_mask_mode == "full":
|
| 398 |
+
proposed_state = observation_encoder_outputs.last_hidden_state[:, : self.z_slots, :].to(
|
| 399 |
+
dtype=recurrent_dtype
|
| 400 |
+
)
|
| 401 |
+
proposal_mask = torch.ones(batch_size, self.z_slots, dtype=torch.long, device=z_state.device)
|
| 402 |
+
observation_outputs = None
|
| 403 |
+
else:
|
| 404 |
+
observation_outputs = observation_encoder_outputs.last_hidden_state[:, self.z_slots :, :].to(
|
| 405 |
+
dtype=recurrent_dtype
|
| 406 |
+
)
|
| 407 |
+
else:
|
| 408 |
+
observation_encoder_outputs = self.encoder(
|
| 409 |
+
input_ids=observation_input_ids,
|
| 410 |
+
attention_mask=observation_attention_mask.long(),
|
| 411 |
+
return_dict=True,
|
| 412 |
+
)
|
| 413 |
+
observation_outputs = observation_encoder_outputs.last_hidden_state.to(dtype=recurrent_dtype)
|
| 414 |
+
if observation_outputs is not None:
|
| 415 |
+
proposed_state, proposal_mask = self.compress_hidden_states_to_slots(
|
| 416 |
+
observation_outputs,
|
| 417 |
+
observation_attention_mask,
|
| 418 |
+
target_slots=self.z_slots,
|
| 419 |
+
)
|
| 420 |
+
proposed_state = proposed_state.to(dtype=recurrent_dtype)
|
| 421 |
+
if self.latent_attention_mask_mode == "full":
|
| 422 |
+
proposed_state = proposed_state.clone()
|
| 423 |
+
inactive_slot_mask = ~proposal_mask.bool()
|
| 424 |
+
proposed_state[inactive_slot_mask] = z_state[inactive_slot_mask]
|
| 425 |
+
proposal_mask = torch.ones(batch_size, self.z_slots, dtype=torch.long, device=z_state.device)
|
| 426 |
+
|
| 427 |
+
no_observation_mask = proposal_mask.sum(dim=1) <= 0
|
| 428 |
+
if torch.any(no_observation_mask):
|
| 429 |
+
proposed_state = proposed_state.clone()
|
| 430 |
+
proposal_mask = proposal_mask.clone()
|
| 431 |
+
proposed_state[no_observation_mask] = z_state[no_observation_mask]
|
| 432 |
+
if previous_state_mask is None:
|
| 433 |
+
proposal_mask[no_observation_mask] = 1
|
| 434 |
+
else:
|
| 435 |
+
proposal_mask[no_observation_mask] = previous_state_mask[no_observation_mask].to(
|
| 436 |
+
device=z_state.device,
|
| 437 |
+
dtype=torch.long,
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
if not self.preserve_observation_encoder_manifold:
|
| 441 |
+
proposed_state = self.proposed_state_norm(proposed_state)
|
| 442 |
+
|
| 443 |
+
gate_inputs = self.state_gate_input_norm(torch.cat([z_state, proposed_state], dim=-1))
|
| 444 |
+
update_gate = torch.sigmoid(self.state_gate(gate_inputs))
|
| 445 |
+
raw_next_state = update_gate * proposed_state + (1.0 - update_gate) * z_state
|
| 446 |
+
active_slot_mask = proposal_mask.bool().unsqueeze(-1)
|
| 447 |
+
next_state = torch.where(active_slot_mask, raw_next_state, z_state)
|
| 448 |
+
if not self.preserve_observation_encoder_manifold:
|
| 449 |
+
next_state = self.recurrent_state_norm(next_state)
|
| 450 |
+
if previous_state_mask is not None:
|
| 451 |
+
next_state_mask = torch.maximum(
|
| 452 |
+
previous_state_mask.to(device=z_state.device, dtype=torch.long),
|
| 453 |
+
proposal_mask,
|
| 454 |
+
)
|
| 455 |
+
else:
|
| 456 |
+
next_state_mask = proposal_mask
|
| 457 |
+
|
| 458 |
+
pooled_query = self.gate_query.to(dtype=next_state.dtype).expand(batch_size, -1, -1)
|
| 459 |
+
gate_context = self.gate_context_norm(next_state)
|
| 460 |
+
pooled_state, _ = self.gate_pool(pooled_query, gate_context, gate_context, need_weights=False)
|
| 461 |
+
gate_logits = self.gate_head(pooled_state.squeeze(1)).squeeze(-1)
|
| 462 |
+
return next_state, gate_logits, next_state_mask
|
| 463 |
+
|
| 464 |
+
def rollout_step(
|
| 465 |
+
self,
|
| 466 |
+
z_state: torch.Tensor,
|
| 467 |
+
observation_input_ids: torch.Tensor,
|
| 468 |
+
observation_attention_mask: torch.Tensor,
|
| 469 |
+
observation_role_ids: torch.Tensor,
|
| 470 |
+
delta_seconds: torch.Tensor,
|
| 471 |
+
elapsed_seconds: torch.Tensor,
|
| 472 |
+
since_last_user_seconds: torch.Tensor,
|
| 473 |
+
since_last_agent_seconds: torch.Tensor,
|
| 474 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 475 |
+
next_state, gate_logits, _ = self._rollout_step_impl(
|
| 476 |
+
z_state=z_state,
|
| 477 |
+
observation_input_ids=observation_input_ids,
|
| 478 |
+
observation_attention_mask=observation_attention_mask,
|
| 479 |
+
observation_role_ids=observation_role_ids,
|
| 480 |
+
delta_seconds=delta_seconds,
|
| 481 |
+
elapsed_seconds=elapsed_seconds,
|
| 482 |
+
since_last_user_seconds=since_last_user_seconds,
|
| 483 |
+
since_last_agent_seconds=since_last_agent_seconds,
|
| 484 |
+
previous_state_mask=None,
|
| 485 |
+
)
|
| 486 |
+
return next_state, gate_logits
|
| 487 |
+
|
| 488 |
+
def rollout_step_with_mask(
|
| 489 |
+
self,
|
| 490 |
+
z_state: torch.Tensor,
|
| 491 |
+
state_mask: torch.Tensor,
|
| 492 |
+
observation_input_ids: torch.Tensor,
|
| 493 |
+
observation_attention_mask: torch.Tensor,
|
| 494 |
+
observation_role_ids: torch.Tensor,
|
| 495 |
+
delta_seconds: torch.Tensor,
|
| 496 |
+
elapsed_seconds: torch.Tensor,
|
| 497 |
+
since_last_user_seconds: torch.Tensor,
|
| 498 |
+
since_last_agent_seconds: torch.Tensor,
|
| 499 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 500 |
+
return self._rollout_step_impl(
|
| 501 |
+
z_state=z_state,
|
| 502 |
+
observation_input_ids=observation_input_ids,
|
| 503 |
+
observation_attention_mask=observation_attention_mask,
|
| 504 |
+
observation_role_ids=observation_role_ids,
|
| 505 |
+
delta_seconds=delta_seconds,
|
| 506 |
+
elapsed_seconds=elapsed_seconds,
|
| 507 |
+
since_last_user_seconds=since_last_user_seconds,
|
| 508 |
+
since_last_agent_seconds=since_last_agent_seconds,
|
| 509 |
+
previous_state_mask=state_mask,
|
| 510 |
+
)
|
| 511 |
+
|
| 512 |
+
def _resolve_generation_eos_token_id(self) -> int | list[int] | None:
|
| 513 |
+
eos_token_id = getattr(self.backbone.generation_config, "eos_token_id", None)
|
| 514 |
+
if eos_token_id is None:
|
| 515 |
+
eos_token_id = getattr(self.backbone.config, "eos_token_id", None)
|
| 516 |
+
return eos_token_id
|
| 517 |
+
|
| 518 |
+
def _resolve_fill_token_id(self) -> int:
|
| 519 |
+
eos_token_id = self._resolve_generation_eos_token_id()
|
| 520 |
+
if isinstance(eos_token_id, list) and eos_token_id:
|
| 521 |
+
return int(eos_token_id[0])
|
| 522 |
+
if isinstance(eos_token_id, int):
|
| 523 |
+
return eos_token_id
|
| 524 |
+
if self.tokenizer.eos_token_id is not None:
|
| 525 |
+
return int(self.tokenizer.eos_token_id)
|
| 526 |
+
if self.tokenizer.pad_token_id is not None:
|
| 527 |
+
return int(self.tokenizer.pad_token_id)
|
| 528 |
+
return 0
|
| 529 |
+
|
| 530 |
+
def _teacher_forced_decoder_inputs(self, labels: torch.Tensor) -> torch.Tensor:
|
| 531 |
+
if labels.ndim != 2 or labels.size(0) <= 0:
|
| 532 |
+
raise ValueError("labels must be a rank-2 tensor with non-zero batch size.")
|
| 533 |
+
safe_labels = labels.clone()
|
| 534 |
+
safe_labels[safe_labels == -100] = self._resolve_fill_token_id()
|
| 535 |
+
if hasattr(self.backbone, "prepare_decoder_input_ids_from_labels"):
|
| 536 |
+
return self.backbone.prepare_decoder_input_ids_from_labels(labels=safe_labels)
|
| 537 |
+
return self.backbone._shift_right(safe_labels)
|
| 538 |
+
|
| 539 |
+
def _build_self_generated_decoder_inputs(
|
| 540 |
+
self,
|
| 541 |
+
labels: torch.Tensor,
|
| 542 |
+
generated: torch.Tensor,
|
| 543 |
+
*,
|
| 544 |
+
self_generated_prefix_tokens: int,
|
| 545 |
+
) -> torch.Tensor:
|
| 546 |
+
batch_size, target_length = labels.shape
|
| 547 |
+
teacher_inputs = self._teacher_forced_decoder_inputs(labels).to(device=labels.device)
|
| 548 |
+
if target_length <= 0:
|
| 549 |
+
return teacher_inputs
|
| 550 |
+
|
| 551 |
+
prefix_token_count = min(max(self_generated_prefix_tokens, 0), max(target_length - 1, 0))
|
| 552 |
+
if prefix_token_count <= 0:
|
| 553 |
+
return teacher_inputs
|
| 554 |
+
|
| 555 |
+
generated_tokens = generated.to(device=labels.device, dtype=torch.long)
|
| 556 |
+
if generated_tokens.ndim != 2:
|
| 557 |
+
raise ValueError("generated tokens must be rank-2.")
|
| 558 |
+
if generated_tokens.size(0) != batch_size:
|
| 559 |
+
raise ValueError("generated batch size must match labels batch size.")
|
| 560 |
+
if generated_tokens.size(1) > 0 and torch.equal(generated_tokens[:, :1], teacher_inputs[:, :1]):
|
| 561 |
+
generated_tokens = generated_tokens[:, 1:]
|
| 562 |
+
|
| 563 |
+
fill_token_id = self._resolve_fill_token_id()
|
| 564 |
+
if generated_tokens.size(1) < prefix_token_count:
|
| 565 |
+
padding = labels.new_full(
|
| 566 |
+
(batch_size, prefix_token_count - generated_tokens.size(1)),
|
| 567 |
+
fill_value=fill_token_id,
|
| 568 |
+
)
|
| 569 |
+
generated_tokens = torch.cat([generated_tokens, padding], dim=1)
|
| 570 |
+
else:
|
| 571 |
+
generated_tokens = generated_tokens[:, :prefix_token_count]
|
| 572 |
+
|
| 573 |
+
hybrid_inputs = teacher_inputs.clone()
|
| 574 |
+
hybrid_inputs[:, 1 : 1 + prefix_token_count] = generated_tokens
|
| 575 |
+
return hybrid_inputs
|
| 576 |
+
|
| 577 |
+
def decoder_loss(
|
| 578 |
+
self,
|
| 579 |
+
z_state: torch.Tensor,
|
| 580 |
+
labels: torch.Tensor,
|
| 581 |
+
*,
|
| 582 |
+
encoder_attention_mask: torch.Tensor | None = None,
|
| 583 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 584 |
+
if z_state.numel() == 0:
|
| 585 |
+
zero = torch.zeros((), device=self.device)
|
| 586 |
+
return zero, zero
|
| 587 |
+
encoder_mask = (
|
| 588 |
+
encoder_attention_mask.to(device=z_state.device, dtype=torch.long)
|
| 589 |
+
if encoder_attention_mask is not None
|
| 590 |
+
else torch.ones(z_state.shape[:2], dtype=torch.long, device=z_state.device)
|
| 591 |
+
)
|
| 592 |
+
token_count = (labels != -100).sum()
|
| 593 |
+
if token_count.item() == 0:
|
| 594 |
+
zero = torch.zeros((), device=z_state.device)
|
| 595 |
+
return zero, zero
|
| 596 |
+
outputs = self.backbone(
|
| 597 |
+
encoder_outputs=BaseModelOutput(last_hidden_state=z_state.to(dtype=self.backbone_dtype)),
|
| 598 |
+
attention_mask=encoder_mask,
|
| 599 |
+
labels=labels,
|
| 600 |
+
return_dict=True,
|
| 601 |
+
)
|
| 602 |
+
loss_sum = outputs.loss * token_count
|
| 603 |
+
return loss_sum, token_count
|
| 604 |
+
|
| 605 |
+
def decoder_self_generated_loss(
|
| 606 |
+
self,
|
| 607 |
+
z_state: torch.Tensor,
|
| 608 |
+
labels: torch.Tensor,
|
| 609 |
+
*,
|
| 610 |
+
generation_kwargs: dict[str, Any] | None = None,
|
| 611 |
+
self_generated_prefix_tokens: int | None = None,
|
| 612 |
+
encoder_attention_mask: torch.Tensor | None = None,
|
| 613 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 614 |
+
if z_state.numel() == 0:
|
| 615 |
+
zero = torch.zeros((), device=self.device)
|
| 616 |
+
return zero, zero
|
| 617 |
+
|
| 618 |
+
encoder_mask = (
|
| 619 |
+
encoder_attention_mask.to(device=z_state.device, dtype=torch.long)
|
| 620 |
+
if encoder_attention_mask is not None
|
| 621 |
+
else torch.ones(z_state.shape[:2], dtype=torch.long, device=z_state.device)
|
| 622 |
+
)
|
| 623 |
+
token_count = (labels != -100).sum()
|
| 624 |
+
if token_count.item() == 0:
|
| 625 |
+
zero = torch.zeros((), device=z_state.device)
|
| 626 |
+
return zero, zero
|
| 627 |
+
|
| 628 |
+
prefix_token_count = max(0, int(self_generated_prefix_tokens if self_generated_prefix_tokens is not None else labels.shape[1]))
|
| 629 |
+
if prefix_token_count <= 0:
|
| 630 |
+
return self.decoder_loss(z_state, labels, encoder_attention_mask=encoder_attention_mask)
|
| 631 |
+
|
| 632 |
+
effective_generation_kwargs = {
|
| 633 |
+
"max_new_tokens": prefix_token_count,
|
| 634 |
+
"do_sample": False,
|
| 635 |
+
"num_beams": 1,
|
| 636 |
+
"return_dict_in_generate": False,
|
| 637 |
+
}
|
| 638 |
+
if generation_kwargs:
|
| 639 |
+
effective_generation_kwargs.update(generation_kwargs)
|
| 640 |
+
|
| 641 |
+
was_training = self.backbone.training
|
| 642 |
+
if was_training:
|
| 643 |
+
self.backbone.eval()
|
| 644 |
+
try:
|
| 645 |
+
with torch.no_grad():
|
| 646 |
+
generated = self.backbone.generate(
|
| 647 |
+
encoder_outputs=BaseModelOutput(last_hidden_state=z_state.to(dtype=self.backbone_dtype)),
|
| 648 |
+
attention_mask=encoder_mask,
|
| 649 |
+
**effective_generation_kwargs,
|
| 650 |
+
)
|
| 651 |
+
finally:
|
| 652 |
+
if was_training:
|
| 653 |
+
self.backbone.train()
|
| 654 |
+
|
| 655 |
+
decoder_input_ids = self._build_self_generated_decoder_inputs(
|
| 656 |
+
labels,
|
| 657 |
+
generated,
|
| 658 |
+
self_generated_prefix_tokens=prefix_token_count,
|
| 659 |
+
)
|
| 660 |
+
outputs = self.backbone(
|
| 661 |
+
encoder_outputs=BaseModelOutput(last_hidden_state=z_state.to(dtype=self.backbone_dtype)),
|
| 662 |
+
attention_mask=encoder_mask,
|
| 663 |
+
decoder_input_ids=decoder_input_ids,
|
| 664 |
+
return_dict=True,
|
| 665 |
+
)
|
| 666 |
+
logits = outputs.logits.float()
|
| 667 |
+
loss_sum = F.cross_entropy(
|
| 668 |
+
logits.reshape(-1, logits.size(-1)),
|
| 669 |
+
labels.reshape(-1),
|
| 670 |
+
ignore_index=-100,
|
| 671 |
+
reduction="sum",
|
| 672 |
+
)
|
| 673 |
+
return loss_sum, token_count
|
| 674 |
+
|
| 675 |
+
@torch.no_grad()
|
| 676 |
+
def first_token_exact_match_stats(
|
| 677 |
+
self,
|
| 678 |
+
z_state: torch.Tensor,
|
| 679 |
+
labels: torch.Tensor,
|
| 680 |
+
*,
|
| 681 |
+
encoder_attention_mask: torch.Tensor | None = None,
|
| 682 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 683 |
+
if z_state.numel() == 0:
|
| 684 |
+
zero = torch.zeros((), device=self.device)
|
| 685 |
+
return zero, zero
|
| 686 |
+
|
| 687 |
+
first_logits, valid_mask = self.first_token_logits(
|
| 688 |
+
z_state,
|
| 689 |
+
labels,
|
| 690 |
+
encoder_attention_mask=encoder_attention_mask,
|
| 691 |
+
)
|
| 692 |
+
valid_count = valid_mask.sum()
|
| 693 |
+
if valid_count.item() == 0:
|
| 694 |
+
zero = torch.zeros((), device=self.device)
|
| 695 |
+
return zero, zero
|
| 696 |
+
|
| 697 |
+
first_targets = labels[:, 0]
|
| 698 |
+
predicted_first_tokens = first_logits.argmax(dim=-1)
|
| 699 |
+
correct = ((predicted_first_tokens == first_targets) & valid_mask).sum()
|
| 700 |
+
return correct, valid_count
|
| 701 |
+
|
| 702 |
+
def first_token_logits(
|
| 703 |
+
self,
|
| 704 |
+
z_state: torch.Tensor,
|
| 705 |
+
labels: torch.Tensor,
|
| 706 |
+
*,
|
| 707 |
+
encoder_attention_mask: torch.Tensor | None = None,
|
| 708 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 709 |
+
if z_state.numel() == 0:
|
| 710 |
+
empty_logits = torch.zeros((0, 0), device=self.device)
|
| 711 |
+
empty_mask = torch.zeros((0,), dtype=torch.bool, device=self.device)
|
| 712 |
+
return empty_logits, empty_mask
|
| 713 |
+
|
| 714 |
+
first_targets = labels[:, 0]
|
| 715 |
+
valid_mask = first_targets != -100
|
| 716 |
+
encoder_mask = (
|
| 717 |
+
encoder_attention_mask.to(device=z_state.device, dtype=torch.long)
|
| 718 |
+
if encoder_attention_mask is not None
|
| 719 |
+
else torch.ones(z_state.shape[:2], dtype=torch.long, device=z_state.device)
|
| 720 |
+
)
|
| 721 |
+
decoder_input_ids = self._teacher_forced_decoder_inputs(labels)[:, :1].to(device=z_state.device)
|
| 722 |
+
outputs = self.backbone(
|
| 723 |
+
encoder_outputs=BaseModelOutput(last_hidden_state=z_state.to(dtype=self.backbone_dtype)),
|
| 724 |
+
attention_mask=encoder_mask,
|
| 725 |
+
decoder_input_ids=decoder_input_ids,
|
| 726 |
+
return_dict=True,
|
| 727 |
+
)
|
| 728 |
+
return outputs.logits[:, 0, :], valid_mask
|
| 729 |
+
|
| 730 |
+
@torch.no_grad()
|
| 731 |
+
def generate_from_state(
|
| 732 |
+
self,
|
| 733 |
+
z_state: torch.Tensor,
|
| 734 |
+
*,
|
| 735 |
+
encoder_attention_mask: torch.Tensor | None = None,
|
| 736 |
+
**generation_kwargs: Any,
|
| 737 |
+
) -> torch.Tensor:
|
| 738 |
+
encoder_mask = (
|
| 739 |
+
encoder_attention_mask.to(device=z_state.device, dtype=torch.long)
|
| 740 |
+
if encoder_attention_mask is not None
|
| 741 |
+
else torch.ones(z_state.shape[:2], dtype=torch.long, device=z_state.device)
|
| 742 |
+
)
|
| 743 |
+
return self.backbone.generate(
|
| 744 |
+
encoder_outputs=BaseModelOutput(last_hidden_state=z_state.to(dtype=self.backbone_dtype)),
|
| 745 |
+
attention_mask=encoder_mask,
|
| 746 |
+
**generation_kwargs,
|
| 747 |
+
)
|
| 748 |
+
|
| 749 |
+
def save_pretrained(self, output_dir: str | Path, tokenizer: Any | None = None) -> None:
|
| 750 |
+
output_path = Path(output_dir)
|
| 751 |
+
output_path.mkdir(parents=True, exist_ok=True)
|
| 752 |
+
(output_path / "sft_config.json").write_text(
|
| 753 |
+
json.dumps(self.config, indent=2, ensure_ascii=False),
|
| 754 |
+
encoding="utf-8",
|
| 755 |
+
)
|
| 756 |
+
# The T5Gemma backbone exposes tied/shared weights, which raw save_file refuses.
|
| 757 |
+
save_model(self, str(output_path / "model.safetensors"))
|
| 758 |
+
save_file({"z_init": self.z_init.detach().cpu()}, str(output_path / "initial_latent_z.safetensors"))
|
| 759 |
+
active_tokenizer = tokenizer or self.tokenizer
|
| 760 |
+
active_tokenizer.save_pretrained(output_path)
|
| 761 |
+
|
| 762 |
+
@classmethod
|
| 763 |
+
def from_pretrained(
|
| 764 |
+
cls,
|
| 765 |
+
model_path_or_repo_id: str,
|
| 766 |
+
device: str | torch.device = "cpu",
|
| 767 |
+
map_location: str | torch.device = "cpu",
|
| 768 |
+
) -> "ThoughtLoopT5Gemma":
|
| 769 |
+
local_path = Path(model_path_or_repo_id)
|
| 770 |
+
if not local_path.exists():
|
| 771 |
+
local_path = Path(snapshot_download(repo_id=model_path_or_repo_id))
|
| 772 |
+
|
| 773 |
+
config = json.loads((local_path / "sft_config.json").read_text(encoding="utf-8"))
|
| 774 |
+
model = cls(config)
|
| 775 |
+
load_model(model, str(local_path / "model.safetensors"), device=str(map_location))
|
| 776 |
+
model.to(device)
|
| 777 |
+
return model
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0f7b8bf028336d8f0695dc3fd48cbe296efc8501953bca95d3a8727f3a3e86a1
|
| 3 |
+
size 2518360980
|
sft_b200.yaml
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
dataset:
|
| 2 |
+
raw_repo_id: BRlkl/timed_natural_conversations_5min4
|
| 3 |
+
cleaned_repo_id: BRlkl/timed_natural_conversations_5min4
|
| 4 |
+
cleaned_local_parquet_dir: artifacts/timed_natural_conversations_5min4/parquet
|
| 5 |
+
source_split: train
|
| 6 |
+
validation_ratio: 0.02
|
| 7 |
+
split_seed: 17
|
| 8 |
+
use_base_chat_template: true
|
| 9 |
+
|
| 10 |
+
model:
|
| 11 |
+
base_model_name: google/t5gemma-l-l-prefixlm-it
|
| 12 |
+
initial_model_path: BRlkl/test_multiturn2
|
| 13 |
+
dtype: bfloat16
|
| 14 |
+
attn_implementation: sdpa
|
| 15 |
+
magicnorm_eps: 1.0e-6
|
| 16 |
+
z_slots: 256
|
| 17 |
+
num_time_tokens: 0
|
| 18 |
+
use_explicit_time_features: false
|
| 19 |
+
gate_attention_heads: 4
|
| 20 |
+
max_observation_tokens: 256
|
| 21 |
+
max_decoder_tokens: 256
|
| 22 |
+
thought_loop_proposal_mode: observation_hidden_compression
|
| 23 |
+
preserve_observation_encoder_manifold: true
|
| 24 |
+
observation_encoder_use_state_context: true
|
| 25 |
+
latent_attention_mask_mode: full
|
| 26 |
+
initial_update_gate_bias: 6.0
|
| 27 |
+
|
| 28 |
+
rollout:
|
| 29 |
+
tick_seconds: 1.0
|
| 30 |
+
chunk_ticks: 300
|
| 31 |
+
max_horizon_ticks: 300
|
| 32 |
+
sort_by_duration_bucket: false
|
| 33 |
+
drop_overlong_examples: true
|
| 34 |
+
long_window_strategy: tail
|
| 35 |
+
post_speech_feedback_delay_seconds: 1.0
|
| 36 |
+
|
| 37 |
+
training:
|
| 38 |
+
seed: 17
|
| 39 |
+
micro_batch_size: 12
|
| 40 |
+
eval_batch_size: 1
|
| 41 |
+
gradient_accumulation_steps: 3
|
| 42 |
+
num_workers: 0
|
| 43 |
+
num_train_epochs: 1
|
| 44 |
+
learning_rate: 5.0e-6
|
| 45 |
+
weight_decay: 0.05
|
| 46 |
+
adam_beta1: 0.9
|
| 47 |
+
adam_beta2: 0.95
|
| 48 |
+
adam_epsilon: 1.0e-8
|
| 49 |
+
warmup_ratio: 0.03
|
| 50 |
+
gradient_checkpointing: true
|
| 51 |
+
fused_adamw: true
|
| 52 |
+
mixed_precision: bf16
|
| 53 |
+
max_grad_norm: 0.1
|
| 54 |
+
tbptt_start_steps: 300
|
| 55 |
+
tbptt_end_steps: 300
|
| 56 |
+
gate_loss_mode: bce
|
| 57 |
+
gate_soft_target_early_window_ticks: 5
|
| 58 |
+
gate_soft_target_late_window_ticks: 5
|
| 59 |
+
gate_soft_target_tau_ticks: 1.5
|
| 60 |
+
gate_soft_target_shape: gaussian
|
| 61 |
+
gate_soft_target_peak: 1.0
|
| 62 |
+
gate_soft_target_floor: 0.0
|
| 63 |
+
gate_soft_target_block_user_ticks: true
|
| 64 |
+
hazard_soft_target_window_ticks: 16
|
| 65 |
+
hazard_soft_target_tau_ticks: 4.0
|
| 66 |
+
hazard_censor_weight: 0.25
|
| 67 |
+
use_ranked_gate_objective: false
|
| 68 |
+
gate_bce_weight: 1.0
|
| 69 |
+
gate_ranking_weight: 1.0
|
| 70 |
+
gate_ranking_margin: 0.5
|
| 71 |
+
gate_ranking_lookback: 64
|
| 72 |
+
gradual_unfreeze_decoder: false
|
| 73 |
+
decoder_initial_trainable_fraction: 0.0
|
| 74 |
+
decoder_unfreeze_start_fraction: 0.0
|
| 75 |
+
decoder_unfreeze_end_fraction: 0.5
|
| 76 |
+
freeze_gate_head_first_half: false
|
| 77 |
+
gate_head_freeze_fraction: 0.5
|
| 78 |
+
gate_teacher_logit_scale: 20.0
|
| 79 |
+
gate_positive_weight: 16.0
|
| 80 |
+
use_dynamic_gate_positive_weight: false
|
| 81 |
+
dynamic_gate_positive_weight_min: 8.0
|
| 82 |
+
dynamic_gate_positive_weight_max: 16.0
|
| 83 |
+
gate_loss_weight: 0.75
|
| 84 |
+
decoder_loss_weight: 1.0
|
| 85 |
+
state_delta_weight: 0.1
|
| 86 |
+
shuffle_train: true
|
| 87 |
+
sort_dataset_by_length: false
|
| 88 |
+
logging_steps: 1
|
| 89 |
+
eval_steps: 51
|
| 90 |
+
checkpoint_steps: 50
|
| 91 |
+
eval_max_batches: 12
|
| 92 |
+
|
| 93 |
+
cache:
|
| 94 |
+
preprocessed_root: cache/preprocessed_5min
|
| 95 |
+
|
| 96 |
+
paths:
|
| 97 |
+
run_root: runs
|
| 98 |
+
export_root: exports
|
| 99 |
+
|
| 100 |
+
inference:
|
| 101 |
+
use_base_chat_template: true
|
| 102 |
+
|
| 103 |
+
wandb:
|
| 104 |
+
enabled: true
|
| 105 |
+
project: samantha-sft-5min
|
| 106 |
+
run_name: t5gemma2-thoughtloop-5min-from-multiturn3
|
| 107 |
+
|
| 108 |
+
hub:
|
| 109 |
+
model_repo_id: BRlkl/test-true2
|
| 110 |
+
private: false
|
| 111 |
+
push_checkpoints: false
|
| 112 |
+
checkpoint_path_prefix: checkpoints
|
sft_config.json
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dataset": {
|
| 3 |
+
"raw_repo_id": "BRlkl/timed_natural_conversations_5min4",
|
| 4 |
+
"cleaned_repo_id": "BRlkl/timed_natural_conversations_5min4",
|
| 5 |
+
"cleaned_local_parquet_dir": "artifacts/timed_natural_conversations_5min4/parquet",
|
| 6 |
+
"source_split": "train",
|
| 7 |
+
"validation_ratio": 0.02,
|
| 8 |
+
"split_seed": 17,
|
| 9 |
+
"use_base_chat_template": true
|
| 10 |
+
},
|
| 11 |
+
"model": {
|
| 12 |
+
"base_model_name": "google/t5gemma-l-l-prefixlm-it",
|
| 13 |
+
"initial_model_path": "BRlkl/test_multiturn2",
|
| 14 |
+
"dtype": "bfloat16",
|
| 15 |
+
"attn_implementation": "sdpa",
|
| 16 |
+
"magicnorm_eps": 1e-06,
|
| 17 |
+
"z_slots": 256,
|
| 18 |
+
"num_time_tokens": 0,
|
| 19 |
+
"use_explicit_time_features": false,
|
| 20 |
+
"gate_attention_heads": 4,
|
| 21 |
+
"max_observation_tokens": 256,
|
| 22 |
+
"max_decoder_tokens": 256,
|
| 23 |
+
"thought_loop_proposal_mode": "observation_hidden_compression",
|
| 24 |
+
"preserve_observation_encoder_manifold": true,
|
| 25 |
+
"observation_encoder_use_state_context": true,
|
| 26 |
+
"latent_attention_mask_mode": "full",
|
| 27 |
+
"initial_update_gate_bias": 6.0
|
| 28 |
+
},
|
| 29 |
+
"rollout": {
|
| 30 |
+
"tick_seconds": 1.0,
|
| 31 |
+
"chunk_ticks": 300,
|
| 32 |
+
"max_horizon_ticks": 300,
|
| 33 |
+
"sort_by_duration_bucket": false,
|
| 34 |
+
"drop_overlong_examples": true,
|
| 35 |
+
"long_window_strategy": "tail",
|
| 36 |
+
"post_speech_feedback_delay_seconds": 1.0
|
| 37 |
+
},
|
| 38 |
+
"training": {
|
| 39 |
+
"seed": 17,
|
| 40 |
+
"micro_batch_size": 12,
|
| 41 |
+
"eval_batch_size": 1,
|
| 42 |
+
"gradient_accumulation_steps": 3,
|
| 43 |
+
"num_workers": 0,
|
| 44 |
+
"num_train_epochs": 1,
|
| 45 |
+
"learning_rate": 5e-06,
|
| 46 |
+
"weight_decay": 0.05,
|
| 47 |
+
"adam_beta1": 0.9,
|
| 48 |
+
"adam_beta2": 0.95,
|
| 49 |
+
"adam_epsilon": 1e-08,
|
| 50 |
+
"warmup_ratio": 0.03,
|
| 51 |
+
"gradient_checkpointing": true,
|
| 52 |
+
"fused_adamw": true,
|
| 53 |
+
"mixed_precision": "bf16",
|
| 54 |
+
"max_grad_norm": 0.1,
|
| 55 |
+
"tbptt_start_steps": 300,
|
| 56 |
+
"tbptt_end_steps": 300,
|
| 57 |
+
"gate_loss_mode": "bce",
|
| 58 |
+
"gate_soft_target_early_window_ticks": 5,
|
| 59 |
+
"gate_soft_target_late_window_ticks": 5,
|
| 60 |
+
"gate_soft_target_tau_ticks": 1.5,
|
| 61 |
+
"gate_soft_target_shape": "gaussian",
|
| 62 |
+
"gate_soft_target_peak": 1.0,
|
| 63 |
+
"gate_soft_target_floor": 0.0,
|
| 64 |
+
"gate_soft_target_block_user_ticks": true,
|
| 65 |
+
"hazard_soft_target_window_ticks": 16,
|
| 66 |
+
"hazard_soft_target_tau_ticks": 4.0,
|
| 67 |
+
"hazard_censor_weight": 0.25,
|
| 68 |
+
"use_ranked_gate_objective": false,
|
| 69 |
+
"gate_bce_weight": 1.0,
|
| 70 |
+
"gate_ranking_weight": 1.0,
|
| 71 |
+
"gate_ranking_margin": 0.5,
|
| 72 |
+
"gate_ranking_lookback": 64,
|
| 73 |
+
"gradual_unfreeze_decoder": false,
|
| 74 |
+
"decoder_initial_trainable_fraction": 0.0,
|
| 75 |
+
"decoder_unfreeze_start_fraction": 0.0,
|
| 76 |
+
"decoder_unfreeze_end_fraction": 0.5,
|
| 77 |
+
"freeze_gate_head_first_half": false,
|
| 78 |
+
"gate_head_freeze_fraction": 0.5,
|
| 79 |
+
"gate_teacher_logit_scale": 20.0,
|
| 80 |
+
"gate_positive_weight": 16.0,
|
| 81 |
+
"use_dynamic_gate_positive_weight": false,
|
| 82 |
+
"dynamic_gate_positive_weight_min": 8.0,
|
| 83 |
+
"dynamic_gate_positive_weight_max": 16.0,
|
| 84 |
+
"gate_loss_weight": 0.75,
|
| 85 |
+
"decoder_loss_weight": 1.0,
|
| 86 |
+
"state_delta_weight": 0.1,
|
| 87 |
+
"shuffle_train": true,
|
| 88 |
+
"sort_dataset_by_length": false,
|
| 89 |
+
"logging_steps": 1,
|
| 90 |
+
"eval_steps": 51,
|
| 91 |
+
"checkpoint_steps": 50,
|
| 92 |
+
"eval_max_batches": 12
|
| 93 |
+
},
|
| 94 |
+
"cache": {
|
| 95 |
+
"preprocessed_root": "cache/preprocessed_5min"
|
| 96 |
+
},
|
| 97 |
+
"paths": {
|
| 98 |
+
"run_root": "runs",
|
| 99 |
+
"export_root": "exports"
|
| 100 |
+
},
|
| 101 |
+
"inference": {
|
| 102 |
+
"use_base_chat_template": true
|
| 103 |
+
},
|
| 104 |
+
"wandb": {
|
| 105 |
+
"enabled": true,
|
| 106 |
+
"project": "samantha-sft-5min",
|
| 107 |
+
"run_name": "t5gemma2-thoughtloop-5min-from-multiturn3"
|
| 108 |
+
},
|
| 109 |
+
"hub": {
|
| 110 |
+
"model_repo_id": "BRlkl/test-true2",
|
| 111 |
+
"private": false,
|
| 112 |
+
"push_checkpoints": false,
|
| 113 |
+
"checkpoint_path_prefix": "checkpoints"
|
| 114 |
+
}
|
| 115 |
+
}
|
tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:923de64f4705efd22f6880dc5e6a9b2993de2706a03d0d48773e0173e32a074f
|
| 3 |
+
size 34362428
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"backend": "tokenizers",
|
| 3 |
+
"bos_token": "<bos>",
|
| 4 |
+
"clean_up_tokenization_spaces": false,
|
| 5 |
+
"eos_token": "<eos>",
|
| 6 |
+
"is_local": false,
|
| 7 |
+
"local_files_only": false,
|
| 8 |
+
"mask_token": "<mask>",
|
| 9 |
+
"model_max_length": 1000000000000000019884624838656,
|
| 10 |
+
"pad_token": "<pad>",
|
| 11 |
+
"padding_side": "right",
|
| 12 |
+
"sp_model_kwargs": {},
|
| 13 |
+
"spaces_between_special_tokens": false,
|
| 14 |
+
"tokenizer_class": "GemmaTokenizer",
|
| 15 |
+
"unk_token": "<unk>",
|
| 16 |
+
"use_default_system_prompt": false
|
| 17 |
+
}
|
train.py
ADDED
|
@@ -0,0 +1,1402 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import math
|
| 6 |
+
import os
|
| 7 |
+
import random
|
| 8 |
+
import secrets
|
| 9 |
+
import shutil
|
| 10 |
+
import sys
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Any
|
| 13 |
+
|
| 14 |
+
import numpy as np
|
| 15 |
+
import torch
|
| 16 |
+
import torch.nn.functional as F
|
| 17 |
+
from accelerate import Accelerator
|
| 18 |
+
from huggingface_hub import HfApi
|
| 19 |
+
from torch.utils.data import DataLoader
|
| 20 |
+
from tqdm.auto import tqdm
|
| 21 |
+
from transformers import get_cosine_schedule_with_warmup
|
| 22 |
+
|
| 23 |
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
| 24 |
+
if str(SCRIPT_DIR) not in sys.path:
|
| 25 |
+
sys.path.insert(0, str(SCRIPT_DIR))
|
| 26 |
+
|
| 27 |
+
from config import ensure_dir, flatten_for_wandb, load_config, save_json
|
| 28 |
+
from data import (
|
| 29 |
+
OBS_ROLE_NONE,
|
| 30 |
+
OBS_ROLE_USER,
|
| 31 |
+
ThoughtLoopConversationDataset,
|
| 32 |
+
build_chunk_batch,
|
| 33 |
+
identity_collate,
|
| 34 |
+
resolve_bucket_horizon_ticks,
|
| 35 |
+
)
|
| 36 |
+
from model import ThoughtLoopT5Gemma
|
| 37 |
+
|
| 38 |
+
try:
|
| 39 |
+
import wandb
|
| 40 |
+
except ImportError: # pragma: no cover
|
| 41 |
+
wandb = None
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def parse_args() -> argparse.Namespace:
|
| 45 |
+
parser = argparse.ArgumentParser(description="Train the recurrent T5Gemma thought-loop model.")
|
| 46 |
+
parser.add_argument("--config", default="configs/sft_b200.yaml")
|
| 47 |
+
parser.add_argument(
|
| 48 |
+
"--resume-from-checkpoint",
|
| 49 |
+
default=None,
|
| 50 |
+
help="Local checkpoint path to resume from, or 'latest'/'auto' to use the newest checkpoint in the run dir.",
|
| 51 |
+
)
|
| 52 |
+
parser.add_argument(
|
| 53 |
+
"--wandb-run-id",
|
| 54 |
+
default=None,
|
| 55 |
+
help="Optional W&B run id to resume. If omitted, train.py reuses a stored/local run id when available.",
|
| 56 |
+
)
|
| 57 |
+
return parser.parse_args()
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def set_seed(seed: int) -> None:
|
| 61 |
+
random.seed(seed)
|
| 62 |
+
np.random.seed(seed)
|
| 63 |
+
torch.manual_seed(seed)
|
| 64 |
+
torch.cuda.manual_seed_all(seed)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def build_optimizer(model: torch.nn.Module, training_cfg: dict[str, Any]) -> torch.optim.Optimizer:
|
| 68 |
+
no_decay_terms = ("bias", "norm", "Norm")
|
| 69 |
+
decay_params = []
|
| 70 |
+
no_decay_params = []
|
| 71 |
+
for name, parameter in model.named_parameters():
|
| 72 |
+
target = no_decay_params if any(term in name for term in no_decay_terms) else decay_params
|
| 73 |
+
target.append(parameter)
|
| 74 |
+
|
| 75 |
+
fused = bool(training_cfg.get("fused_adamw", True) and torch.cuda.is_available())
|
| 76 |
+
return torch.optim.AdamW(
|
| 77 |
+
[
|
| 78 |
+
{"params": decay_params, "weight_decay": float(training_cfg["weight_decay"])},
|
| 79 |
+
{"params": no_decay_params, "weight_decay": 0.0},
|
| 80 |
+
],
|
| 81 |
+
lr=float(training_cfg["learning_rate"]),
|
| 82 |
+
betas=(float(training_cfg["adam_beta1"]), float(training_cfg["adam_beta2"])),
|
| 83 |
+
eps=float(training_cfg["adam_epsilon"]),
|
| 84 |
+
fused=fused,
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def resolve_bucket_tbptt_steps(training_cfg: dict[str, Any], duration_bucket: str) -> int | None:
|
| 89 |
+
bucket_map = training_cfg.get("tbptt_steps_by_bucket", {})
|
| 90 |
+
if duration_bucket in bucket_map:
|
| 91 |
+
return int(bucket_map[duration_bucket])
|
| 92 |
+
|
| 93 |
+
start_steps = int(training_cfg.get("tbptt_start_steps", 0))
|
| 94 |
+
end_steps = int(training_cfg.get("tbptt_end_steps", start_steps))
|
| 95 |
+
if start_steps <= 0 or end_steps <= 0:
|
| 96 |
+
return None
|
| 97 |
+
return end_steps
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def resolve_linear_schedule_value(
|
| 101 |
+
*,
|
| 102 |
+
enabled: bool,
|
| 103 |
+
global_step: int,
|
| 104 |
+
total_update_steps: int,
|
| 105 |
+
start_fraction: float,
|
| 106 |
+
end_fraction: float,
|
| 107 |
+
start_value: float,
|
| 108 |
+
end_value: float,
|
| 109 |
+
) -> float | None:
|
| 110 |
+
if not enabled:
|
| 111 |
+
return None
|
| 112 |
+
|
| 113 |
+
clamped_total_steps = max(total_update_steps, 1)
|
| 114 |
+
start_step = int(round(clamped_total_steps * start_fraction))
|
| 115 |
+
end_step = int(round(clamped_total_steps * end_fraction))
|
| 116 |
+
if end_step <= start_step:
|
| 117 |
+
end_step = start_step + 1
|
| 118 |
+
|
| 119 |
+
if global_step <= start_step:
|
| 120 |
+
return start_value
|
| 121 |
+
if global_step >= end_step:
|
| 122 |
+
return end_value
|
| 123 |
+
|
| 124 |
+
progress = (global_step - start_step) / max(end_step - start_step, 1)
|
| 125 |
+
return start_value + progress * (end_value - start_value)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def should_freeze_gate_head(
|
| 129 |
+
training_cfg: dict[str, Any],
|
| 130 |
+
global_step: int,
|
| 131 |
+
total_update_steps: int,
|
| 132 |
+
) -> bool:
|
| 133 |
+
if not bool(training_cfg.get("freeze_gate_head_first_half", False)):
|
| 134 |
+
return False
|
| 135 |
+
freeze_fraction = float(training_cfg.get("gate_head_freeze_fraction", 0.5))
|
| 136 |
+
return global_step < int(round(max(total_update_steps, 1) * freeze_fraction))
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def resolve_decoder_trainable_fraction(
|
| 140 |
+
training_cfg: dict[str, Any],
|
| 141 |
+
global_step: int,
|
| 142 |
+
total_update_steps: int,
|
| 143 |
+
) -> float | None:
|
| 144 |
+
return resolve_linear_schedule_value(
|
| 145 |
+
enabled=bool(training_cfg.get("gradual_unfreeze_decoder", False)),
|
| 146 |
+
global_step=global_step,
|
| 147 |
+
total_update_steps=total_update_steps,
|
| 148 |
+
start_fraction=float(training_cfg.get("decoder_unfreeze_start_fraction", 0.0)),
|
| 149 |
+
end_fraction=float(training_cfg.get("decoder_unfreeze_end_fraction", 0.5)),
|
| 150 |
+
start_value=float(training_cfg.get("decoder_initial_trainable_fraction", 0.0)),
|
| 151 |
+
end_value=1.0,
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def resolve_batch_duration_bucket(conversations: list[dict[str, Any]]) -> str:
|
| 156 |
+
if not conversations:
|
| 157 |
+
return "short"
|
| 158 |
+
buckets = {str(conversation["duration_bucket"]) for conversation in conversations}
|
| 159 |
+
if len(buckets) == 1:
|
| 160 |
+
return next(iter(buckets))
|
| 161 |
+
return "mixed"
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def ensure_single_chunk_dataset(dataset: ThoughtLoopConversationDataset, split: str) -> None:
|
| 165 |
+
offending_examples = [
|
| 166 |
+
example for example in dataset.examples if int(example.get("chunk_count", 0)) != 1
|
| 167 |
+
]
|
| 168 |
+
if offending_examples:
|
| 169 |
+
raise ValueError(
|
| 170 |
+
f"{split} dataset expected exactly one chunk per conversation, but found "
|
| 171 |
+
f"{len(offending_examples)} multi-chunk examples. Check chunk_ticks/max_horizon_ticks."
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def maybe_sort_dataset_by_length(
|
| 176 |
+
dataset: ThoughtLoopConversationDataset,
|
| 177 |
+
*,
|
| 178 |
+
enabled: bool,
|
| 179 |
+
) -> None:
|
| 180 |
+
if not enabled:
|
| 181 |
+
return
|
| 182 |
+
dataset.examples.sort(
|
| 183 |
+
key=lambda item: (
|
| 184 |
+
int(item.get("effective_total_ticks", item.get("total_ticks", 0))),
|
| 185 |
+
int(item.get("total_ticks", 0)),
|
| 186 |
+
str(item.get("row_id", "")),
|
| 187 |
+
)
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def move_chunk_batch_to_device(batch: dict[str, torch.Tensor], device: torch.device) -> dict[str, torch.Tensor]:
|
| 192 |
+
return {key: value.to(device) for key, value in batch.items()}
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def apply_runtime_model_config(model: ThoughtLoopT5Gemma, config: dict[str, Any]) -> None:
|
| 196 |
+
model.config = config
|
| 197 |
+
model_cfg = config["model"]
|
| 198 |
+
expected_z_slots = int(model_cfg["z_slots"])
|
| 199 |
+
if int(model.z_slots) != expected_z_slots:
|
| 200 |
+
raise ValueError(
|
| 201 |
+
f"Loaded model has z_slots={model.z_slots}, but config requested z_slots={expected_z_slots}."
|
| 202 |
+
)
|
| 203 |
+
model.thought_loop_proposal_mode = str(
|
| 204 |
+
model_cfg.get("thought_loop_proposal_mode", model.thought_loop_proposal_mode)
|
| 205 |
+
).strip().lower()
|
| 206 |
+
model.preserve_observation_encoder_manifold = bool(
|
| 207 |
+
model_cfg.get(
|
| 208 |
+
"preserve_observation_encoder_manifold",
|
| 209 |
+
model.thought_loop_proposal_mode == "observation_hidden_compression",
|
| 210 |
+
)
|
| 211 |
+
)
|
| 212 |
+
model.observation_encoder_use_state_context = bool(
|
| 213 |
+
model_cfg.get("observation_encoder_use_state_context", False)
|
| 214 |
+
)
|
| 215 |
+
model.latent_attention_mask_mode = str(
|
| 216 |
+
model_cfg.get("latent_attention_mask_mode", getattr(model, "latent_attention_mask_mode", "observed"))
|
| 217 |
+
).strip().lower()
|
| 218 |
+
if model.latent_attention_mask_mode not in {"observed", "full"}:
|
| 219 |
+
raise ValueError(f"Unsupported latent_attention_mask_mode: {model.latent_attention_mask_mode}")
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def build_initial_model(config: dict[str, Any]) -> ThoughtLoopT5Gemma:
|
| 223 |
+
initial_model_path = config["model"].get("initial_model_path")
|
| 224 |
+
if initial_model_path:
|
| 225 |
+
model = ThoughtLoopT5Gemma.from_pretrained(str(initial_model_path), device="cpu", map_location="cpu")
|
| 226 |
+
apply_runtime_model_config(model, config)
|
| 227 |
+
return model
|
| 228 |
+
return ThoughtLoopT5Gemma(config)
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def rollout_active_rows_only(
|
| 232 |
+
*,
|
| 233 |
+
model: ThoughtLoopT5Gemma,
|
| 234 |
+
batch: dict[str, torch.Tensor],
|
| 235 |
+
z_state: torch.Tensor,
|
| 236 |
+
state_mask: torch.Tensor,
|
| 237 |
+
step_index: int,
|
| 238 |
+
active_mask: torch.Tensor,
|
| 239 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 240 |
+
active_indices = torch.nonzero(active_mask, as_tuple=False).flatten()
|
| 241 |
+
active_next_z, active_gate_logits, active_next_mask = model.rollout_step_with_mask(
|
| 242 |
+
z_state=z_state[active_indices],
|
| 243 |
+
state_mask=state_mask[active_indices],
|
| 244 |
+
observation_input_ids=batch["observation_input_ids"][active_indices, step_index, :],
|
| 245 |
+
observation_attention_mask=batch["observation_attention_mask"][active_indices, step_index, :],
|
| 246 |
+
observation_role_ids=batch["observation_role"][active_indices, step_index],
|
| 247 |
+
delta_seconds=batch["delta_seconds"][active_indices, step_index],
|
| 248 |
+
elapsed_seconds=batch["elapsed_seconds"][active_indices, step_index],
|
| 249 |
+
since_last_user_seconds=torch.zeros_like(batch["elapsed_seconds"][active_indices, step_index]),
|
| 250 |
+
since_last_agent_seconds=torch.zeros_like(batch["elapsed_seconds"][active_indices, step_index]),
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
next_z = z_state.clone()
|
| 254 |
+
next_z[active_indices] = active_next_z
|
| 255 |
+
next_state_mask = state_mask.clone()
|
| 256 |
+
next_state_mask[active_indices] = active_next_mask
|
| 257 |
+
gate_logits = torch.zeros(batch["tick_mask"].shape[0], device=z_state.device, dtype=active_gate_logits.dtype)
|
| 258 |
+
gate_logits[active_indices] = active_gate_logits
|
| 259 |
+
return next_z, next_state_mask, gate_logits
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def compute_gate_positive_weight(batch: dict[str, torch.Tensor], training_cfg: dict[str, Any]) -> torch.Tensor:
|
| 263 |
+
if not bool(training_cfg.get("use_dynamic_gate_positive_weight", True)):
|
| 264 |
+
return torch.tensor(float(training_cfg["gate_positive_weight"]), device=batch["tick_mask"].device)
|
| 265 |
+
|
| 266 |
+
active_targets = batch["gate_target"][batch["tick_mask"]]
|
| 267 |
+
positive_count = active_targets.sum()
|
| 268 |
+
negative_count = active_targets.numel() - positive_count
|
| 269 |
+
if positive_count.item() <= 0.0:
|
| 270 |
+
fallback = float(training_cfg["gate_positive_weight"])
|
| 271 |
+
return torch.tensor(fallback, device=batch["tick_mask"].device)
|
| 272 |
+
|
| 273 |
+
dynamic_weight = negative_count / positive_count.clamp_min(1.0)
|
| 274 |
+
min_weight = float(training_cfg.get("dynamic_gate_positive_weight_min", 1.0))
|
| 275 |
+
max_weight = float(training_cfg.get("dynamic_gate_positive_weight_max", 256.0))
|
| 276 |
+
dynamic_weight = dynamic_weight.clamp(min=min_weight, max=max_weight)
|
| 277 |
+
return dynamic_weight.to(device=batch["tick_mask"].device)
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def compute_distance_aware_hazard_gate_loss(
|
| 281 |
+
gate_logits: torch.Tensor,
|
| 282 |
+
gate_targets: torch.Tensor,
|
| 283 |
+
gate_loss_mask: torch.Tensor,
|
| 284 |
+
training_cfg: dict[str, Any],
|
| 285 |
+
) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:
|
| 286 |
+
device = gate_logits.device
|
| 287 |
+
batch_size, max_steps = gate_logits.shape
|
| 288 |
+
support_window_ticks = int(training_cfg.get("hazard_soft_target_window_ticks", 16))
|
| 289 |
+
target_tau_ticks = float(training_cfg.get("hazard_soft_target_tau_ticks", 4.0))
|
| 290 |
+
censor_weight = float(training_cfg.get("hazard_censor_weight", 0.25))
|
| 291 |
+
|
| 292 |
+
log_hazard = F.logsigmoid(gate_logits)
|
| 293 |
+
log_survival = F.logsigmoid(-gate_logits)
|
| 294 |
+
|
| 295 |
+
gate_loss_sum = torch.zeros((), device=device)
|
| 296 |
+
gate_loss_count = torch.zeros((), device=device)
|
| 297 |
+
hazard_event_count = torch.zeros((), device=device)
|
| 298 |
+
hazard_censor_count = torch.zeros((), device=device)
|
| 299 |
+
|
| 300 |
+
for batch_index in range(batch_size):
|
| 301 |
+
active_len = int(gate_loss_mask[batch_index].sum().item())
|
| 302 |
+
if active_len <= 0:
|
| 303 |
+
continue
|
| 304 |
+
|
| 305 |
+
active_targets = gate_targets[batch_index, :active_len]
|
| 306 |
+
active_log_hazard = log_hazard[batch_index, :active_len]
|
| 307 |
+
active_log_survival = log_survival[batch_index, :active_len]
|
| 308 |
+
positive_indices = torch.nonzero(active_targets > 0.5, as_tuple=False).flatten().tolist()
|
| 309 |
+
|
| 310 |
+
segment_start = 0
|
| 311 |
+
for positive_index in positive_indices:
|
| 312 |
+
segment_log_hazard = active_log_hazard[segment_start : positive_index + 1]
|
| 313 |
+
segment_log_survival = active_log_survival[segment_start : positive_index + 1]
|
| 314 |
+
if segment_log_hazard.numel() <= 0:
|
| 315 |
+
segment_start = positive_index + 1
|
| 316 |
+
continue
|
| 317 |
+
|
| 318 |
+
prefix_log_survival = torch.cumsum(segment_log_survival, dim=0) - segment_log_survival
|
| 319 |
+
segment_log_event = prefix_log_survival + segment_log_hazard
|
| 320 |
+
|
| 321 |
+
if support_window_ticks > 0:
|
| 322 |
+
support_start = max(0, segment_log_event.numel() - support_window_ticks)
|
| 323 |
+
else:
|
| 324 |
+
support_start = 0
|
| 325 |
+
|
| 326 |
+
support_log_event = segment_log_event[support_start:]
|
| 327 |
+
support_indices = torch.arange(
|
| 328 |
+
support_start,
|
| 329 |
+
segment_log_event.numel(),
|
| 330 |
+
device=device,
|
| 331 |
+
dtype=torch.float32,
|
| 332 |
+
)
|
| 333 |
+
distances = (segment_log_event.numel() - 1) - support_indices
|
| 334 |
+
|
| 335 |
+
if target_tau_ticks > 0.0:
|
| 336 |
+
target_weights = torch.exp(-distances / target_tau_ticks)
|
| 337 |
+
else:
|
| 338 |
+
target_weights = torch.zeros_like(distances)
|
| 339 |
+
target_weights[-1] = 1.0
|
| 340 |
+
target_weights = target_weights / target_weights.sum().clamp_min(1e-8)
|
| 341 |
+
|
| 342 |
+
segment_loss = -(target_weights * support_log_event).sum()
|
| 343 |
+
gate_loss_sum = gate_loss_sum + segment_loss
|
| 344 |
+
gate_loss_count = gate_loss_count + 1.0
|
| 345 |
+
hazard_event_count = hazard_event_count + 1.0
|
| 346 |
+
segment_start = positive_index + 1
|
| 347 |
+
|
| 348 |
+
if segment_start < active_len:
|
| 349 |
+
tail_log_survival = active_log_survival[segment_start:active_len]
|
| 350 |
+
if tail_log_survival.numel() > 0:
|
| 351 |
+
censored_loss = -tail_log_survival.mean()
|
| 352 |
+
gate_loss_sum = gate_loss_sum + censor_weight * censored_loss
|
| 353 |
+
gate_loss_count = gate_loss_count + censor_weight
|
| 354 |
+
hazard_censor_count = hazard_censor_count + 1.0
|
| 355 |
+
|
| 356 |
+
gate_loss = gate_loss_sum / gate_loss_count.clamp_min(1.0)
|
| 357 |
+
metrics = {
|
| 358 |
+
"gate_loss_sum": gate_loss_sum.detach(),
|
| 359 |
+
"gate_count": gate_loss_count.detach(),
|
| 360 |
+
"hazard_event_count": hazard_event_count.detach(),
|
| 361 |
+
"hazard_censor_count": hazard_censor_count.detach(),
|
| 362 |
+
}
|
| 363 |
+
return gate_loss, metrics
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
def build_temporally_soft_gate_targets(
|
| 367 |
+
*,
|
| 368 |
+
gate_targets: torch.Tensor,
|
| 369 |
+
gate_loss_mask: torch.Tensor,
|
| 370 |
+
observation_roles: torch.Tensor,
|
| 371 |
+
training_cfg: dict[str, Any],
|
| 372 |
+
) -> torch.Tensor:
|
| 373 |
+
early_window = max(0, int(training_cfg.get("gate_soft_target_early_window_ticks", 3)))
|
| 374 |
+
late_window = max(0, int(training_cfg.get("gate_soft_target_late_window_ticks", 3)))
|
| 375 |
+
tau_ticks = max(1e-6, float(training_cfg.get("gate_soft_target_tau_ticks", 1.5)))
|
| 376 |
+
peak = float(training_cfg.get("gate_soft_target_peak", 1.0))
|
| 377 |
+
floor = max(0.0, float(training_cfg.get("gate_soft_target_floor", 0.0)))
|
| 378 |
+
shape = str(training_cfg.get("gate_soft_target_shape", "gaussian")).strip().lower()
|
| 379 |
+
block_user_ticks = bool(training_cfg.get("gate_soft_target_block_user_ticks", True))
|
| 380 |
+
|
| 381 |
+
soft_targets = torch.zeros_like(gate_targets)
|
| 382 |
+
candidate_mask = gate_loss_mask > 0
|
| 383 |
+
if block_user_ticks:
|
| 384 |
+
candidate_mask = candidate_mask & (observation_roles != OBS_ROLE_USER)
|
| 385 |
+
|
| 386 |
+
batch_size, _ = gate_targets.shape
|
| 387 |
+
for batch_index in range(batch_size):
|
| 388 |
+
active_len = int(gate_loss_mask[batch_index].sum().item())
|
| 389 |
+
if active_len <= 0:
|
| 390 |
+
continue
|
| 391 |
+
|
| 392 |
+
positive_indices = torch.nonzero(
|
| 393 |
+
gate_targets[batch_index, :active_len] > 0.5,
|
| 394 |
+
as_tuple=False,
|
| 395 |
+
).flatten().tolist()
|
| 396 |
+
for positive_index in positive_indices:
|
| 397 |
+
start_index = max(0, positive_index - early_window)
|
| 398 |
+
end_index = min(active_len, positive_index + late_window + 1)
|
| 399 |
+
if end_index <= start_index:
|
| 400 |
+
continue
|
| 401 |
+
|
| 402 |
+
indices = torch.arange(start_index, end_index, device=gate_targets.device)
|
| 403 |
+
distances = (indices - positive_index).abs().to(dtype=gate_targets.dtype)
|
| 404 |
+
if shape == "exponential":
|
| 405 |
+
values = torch.exp(-distances / tau_ticks)
|
| 406 |
+
elif shape == "linear":
|
| 407 |
+
side_windows = torch.where(
|
| 408 |
+
indices <= positive_index,
|
| 409 |
+
torch.full_like(indices, max(early_window, 1)),
|
| 410 |
+
torch.full_like(indices, max(late_window, 1)),
|
| 411 |
+
).to(dtype=gate_targets.dtype)
|
| 412 |
+
values = 1.0 - distances / (side_windows + 1.0)
|
| 413 |
+
else:
|
| 414 |
+
values = torch.exp(-(distances.pow(2)) / (2.0 * tau_ticks * tau_ticks))
|
| 415 |
+
|
| 416 |
+
values = (values * peak).clamp(0.0, 1.0)
|
| 417 |
+
if floor > 0.0:
|
| 418 |
+
values = torch.where(values > 0.0, values.clamp_min(floor), values)
|
| 419 |
+
|
| 420 |
+
valid = candidate_mask[batch_index, indices]
|
| 421 |
+
if not torch.any(valid):
|
| 422 |
+
continue
|
| 423 |
+
|
| 424 |
+
valid_indices = indices[valid]
|
| 425 |
+
soft_targets[batch_index, valid_indices] = torch.maximum(
|
| 426 |
+
soft_targets[batch_index, valid_indices],
|
| 427 |
+
values[valid],
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
return torch.maximum(soft_targets, gate_targets).clamp(0.0, 1.0)
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
def compute_soft_bce_gate_loss(
|
| 434 |
+
*,
|
| 435 |
+
gate_logits: torch.Tensor,
|
| 436 |
+
hard_gate_targets: torch.Tensor,
|
| 437 |
+
gate_loss_mask: torch.Tensor,
|
| 438 |
+
observation_roles: torch.Tensor,
|
| 439 |
+
positive_weight: torch.Tensor,
|
| 440 |
+
training_cfg: dict[str, Any],
|
| 441 |
+
) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:
|
| 442 |
+
soft_targets = build_temporally_soft_gate_targets(
|
| 443 |
+
gate_targets=hard_gate_targets,
|
| 444 |
+
gate_loss_mask=gate_loss_mask,
|
| 445 |
+
observation_roles=observation_roles,
|
| 446 |
+
training_cfg=training_cfg,
|
| 447 |
+
)
|
| 448 |
+
loss_raw = F.binary_cross_entropy_with_logits(gate_logits, soft_targets, reduction="none")
|
| 449 |
+
soft_weight = 1.0 + (positive_weight.to(device=gate_logits.device) - 1.0) * soft_targets
|
| 450 |
+
masked_loss = loss_raw * soft_weight * gate_loss_mask
|
| 451 |
+
gate_loss_sum = masked_loss.sum()
|
| 452 |
+
gate_count = gate_loss_mask.sum()
|
| 453 |
+
gate_loss = gate_loss_sum / gate_count.clamp_min(1.0)
|
| 454 |
+
metrics = {
|
| 455 |
+
"gate_loss_sum": gate_loss_sum.detach(),
|
| 456 |
+
"gate_count": gate_count.detach(),
|
| 457 |
+
"soft_target_mass": (soft_targets * gate_loss_mask).sum().detach(),
|
| 458 |
+
"soft_target_positive_count": ((soft_targets >= 0.5) & (gate_loss_mask > 0)).float().sum().detach(),
|
| 459 |
+
}
|
| 460 |
+
return gate_loss, metrics
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
def compute_gate_ranking_loss(
|
| 464 |
+
gate_logits: torch.Tensor,
|
| 465 |
+
gate_targets: torch.Tensor,
|
| 466 |
+
gate_loss_mask: torch.Tensor,
|
| 467 |
+
ranking_margin: float,
|
| 468 |
+
ranking_lookback: int,
|
| 469 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 470 |
+
device = gate_logits.device
|
| 471 |
+
ranking_loss_sum = torch.zeros((), device=device)
|
| 472 |
+
ranking_pair_count = torch.zeros((), device=device)
|
| 473 |
+
|
| 474 |
+
batch_size, max_steps = gate_logits.shape
|
| 475 |
+
for batch_index in range(batch_size):
|
| 476 |
+
for step_index in range(max_steps):
|
| 477 |
+
if float(gate_loss_mask[batch_index, step_index]) <= 0.0:
|
| 478 |
+
continue
|
| 479 |
+
if float(gate_targets[batch_index, step_index]) <= 0.5:
|
| 480 |
+
continue
|
| 481 |
+
|
| 482 |
+
positive_logit = gate_logits[batch_index, step_index]
|
| 483 |
+
search_start = max(0, step_index - ranking_lookback)
|
| 484 |
+
for previous_index in range(search_start, step_index):
|
| 485 |
+
if float(gate_loss_mask[batch_index, previous_index]) <= 0.0:
|
| 486 |
+
continue
|
| 487 |
+
if float(gate_targets[batch_index, previous_index]) >= 0.5:
|
| 488 |
+
continue
|
| 489 |
+
|
| 490 |
+
negative_logit = gate_logits[batch_index, previous_index]
|
| 491 |
+
ranking_loss_sum = ranking_loss_sum + F.relu(ranking_margin - positive_logit + negative_logit)
|
| 492 |
+
ranking_pair_count = ranking_pair_count + 1
|
| 493 |
+
|
| 494 |
+
return ranking_loss_sum, ranking_pair_count
|
| 495 |
+
|
| 496 |
+
|
| 497 |
+
def compute_chunk_losses(
|
| 498 |
+
model: ThoughtLoopT5Gemma,
|
| 499 |
+
batch: dict[str, torch.Tensor],
|
| 500 |
+
training_cfg: dict[str, Any],
|
| 501 |
+
z_state: torch.Tensor,
|
| 502 |
+
state_mask: torch.Tensor,
|
| 503 |
+
tbptt_steps: int | None = None,
|
| 504 |
+
freeze_gate_head_to_targets: bool = False,
|
| 505 |
+
) -> tuple[torch.Tensor, dict[str, torch.Tensor], torch.Tensor, torch.Tensor]:
|
| 506 |
+
device = batch["tick_mask"].device
|
| 507 |
+
batch_size, max_steps = batch["tick_mask"].shape
|
| 508 |
+
gate_loss_mode = str(training_cfg.get("gate_loss_mode", "hazard")).strip().lower()
|
| 509 |
+
use_hazard_gate_loss = gate_loss_mode == "hazard"
|
| 510 |
+
use_soft_bce_gate_loss = gate_loss_mode == "soft_bce"
|
| 511 |
+
pos_weight = (
|
| 512 |
+
torch.zeros((), device=device)
|
| 513 |
+
if use_hazard_gate_loss
|
| 514 |
+
else compute_gate_positive_weight(batch, training_cfg)
|
| 515 |
+
)
|
| 516 |
+
|
| 517 |
+
gate_loss_sum = torch.zeros((), device=device)
|
| 518 |
+
gate_count = torch.zeros((), device=device)
|
| 519 |
+
decoder_loss_sum = torch.zeros((), device=device)
|
| 520 |
+
decoder_token_count = torch.zeros((), device=device)
|
| 521 |
+
state_penalty_sum = torch.zeros((), device=device)
|
| 522 |
+
state_penalty_count = torch.zeros((), device=device)
|
| 523 |
+
|
| 524 |
+
tp = torch.zeros((), device=device)
|
| 525 |
+
fp = torch.zeros((), device=device)
|
| 526 |
+
fn = torch.zeros((), device=device)
|
| 527 |
+
correct = torch.zeros((), device=device)
|
| 528 |
+
counted = torch.zeros((), device=device)
|
| 529 |
+
target_positive_count = torch.zeros((), device=device)
|
| 530 |
+
predicted_positive_count = torch.zeros((), device=device)
|
| 531 |
+
|
| 532 |
+
state_delta_weight = float(training_cfg["state_delta_weight"])
|
| 533 |
+
steps_since_detach = 0
|
| 534 |
+
gate_logits_history: list[torch.Tensor] = []
|
| 535 |
+
gate_target_history: list[torch.Tensor] = []
|
| 536 |
+
gate_loss_mask_history: list[torch.Tensor] = []
|
| 537 |
+
observation_role_history: list[torch.Tensor] = []
|
| 538 |
+
hazard_event_count = torch.zeros((), device=device)
|
| 539 |
+
hazard_censor_count = torch.zeros((), device=device)
|
| 540 |
+
soft_target_mass = torch.zeros((), device=device)
|
| 541 |
+
soft_target_positive_count = torch.zeros((), device=device)
|
| 542 |
+
teacher_logit_scale = float(training_cfg.get("gate_teacher_logit_scale", 20.0))
|
| 543 |
+
|
| 544 |
+
for step_index in range(max_steps):
|
| 545 |
+
active_mask = batch["tick_mask"][:, step_index]
|
| 546 |
+
if not torch.any(active_mask):
|
| 547 |
+
break
|
| 548 |
+
|
| 549 |
+
previous_z = z_state
|
| 550 |
+
next_z, next_state_mask, gate_logits = rollout_active_rows_only(
|
| 551 |
+
model=model,
|
| 552 |
+
batch=batch,
|
| 553 |
+
z_state=z_state,
|
| 554 |
+
state_mask=state_mask,
|
| 555 |
+
step_index=step_index,
|
| 556 |
+
active_mask=active_mask,
|
| 557 |
+
)
|
| 558 |
+
z_state = next_z
|
| 559 |
+
state_mask = next_state_mask
|
| 560 |
+
|
| 561 |
+
step_loss_mask = active_mask.float()
|
| 562 |
+
step_targets = batch["gate_target"][:, step_index]
|
| 563 |
+
if freeze_gate_head_to_targets:
|
| 564 |
+
gate_logits = torch.where(
|
| 565 |
+
step_targets > 0.5,
|
| 566 |
+
torch.full_like(step_targets, teacher_logit_scale),
|
| 567 |
+
torch.full_like(step_targets, -teacher_logit_scale),
|
| 568 |
+
)
|
| 569 |
+
|
| 570 |
+
gate_logits_history.append(gate_logits)
|
| 571 |
+
gate_target_history.append(step_targets)
|
| 572 |
+
gate_loss_mask_history.append(step_loss_mask)
|
| 573 |
+
observation_role_history.append(batch["observation_role"][:, step_index])
|
| 574 |
+
|
| 575 |
+
if torch.any(step_loss_mask > 0):
|
| 576 |
+
step_predictions = (torch.sigmoid(gate_logits) >= 0.5).float()
|
| 577 |
+
valid = step_loss_mask > 0
|
| 578 |
+
correct = correct + ((step_predictions == step_targets) & valid).float().sum()
|
| 579 |
+
counted = counted + valid.float().sum()
|
| 580 |
+
tp = tp + ((step_predictions == 1) & (step_targets == 1) & valid).float().sum()
|
| 581 |
+
fp = fp + ((step_predictions == 1) & (step_targets == 0) & valid).float().sum()
|
| 582 |
+
fn = fn + ((step_predictions == 0) & (step_targets == 1) & valid).float().sum()
|
| 583 |
+
target_positive_count = target_positive_count + ((step_targets == 1) & valid).float().sum()
|
| 584 |
+
predicted_positive_count = predicted_positive_count + ((step_predictions == 1) & valid).float().sum()
|
| 585 |
+
|
| 586 |
+
if (
|
| 587 |
+
torch.any(step_loss_mask > 0)
|
| 588 |
+
and not use_hazard_gate_loss
|
| 589 |
+
and not use_soft_bce_gate_loss
|
| 590 |
+
and not freeze_gate_head_to_targets
|
| 591 |
+
):
|
| 592 |
+
gate_loss_raw = F.binary_cross_entropy_with_logits(gate_logits, step_targets, reduction="none")
|
| 593 |
+
gate_weight = torch.where(step_targets > 0.5, pos_weight, torch.ones_like(step_targets))
|
| 594 |
+
gate_loss_sum = gate_loss_sum + (gate_loss_raw * gate_weight * step_loss_mask).sum()
|
| 595 |
+
gate_count = gate_count + step_loss_mask.sum()
|
| 596 |
+
|
| 597 |
+
if state_delta_weight > 0.0:
|
| 598 |
+
state_delta = (next_z - previous_z).pow(2).mean(dim=(1, 2))
|
| 599 |
+
state_penalty_sum = state_penalty_sum + (state_delta * step_loss_mask).sum()
|
| 600 |
+
state_penalty_count = state_penalty_count + step_loss_mask.sum()
|
| 601 |
+
elif torch.any(step_loss_mask > 0) and state_delta_weight > 0.0:
|
| 602 |
+
state_delta = (next_z - previous_z).pow(2).mean(dim=(1, 2))
|
| 603 |
+
state_penalty_sum = state_penalty_sum + (state_delta * step_loss_mask).sum()
|
| 604 |
+
state_penalty_count = state_penalty_count + step_loss_mask.sum()
|
| 605 |
+
|
| 606 |
+
step_labels = batch["decoder_labels"][:, step_index, :]
|
| 607 |
+
decoder_rows = active_mask & torch.any(step_labels != -100, dim=-1)
|
| 608 |
+
if torch.any(decoder_rows):
|
| 609 |
+
step_loss_sum, step_token_count = model.decoder_loss(
|
| 610 |
+
z_state[decoder_rows],
|
| 611 |
+
step_labels[decoder_rows],
|
| 612 |
+
encoder_attention_mask=state_mask[decoder_rows],
|
| 613 |
+
)
|
| 614 |
+
decoder_loss_sum = decoder_loss_sum + step_loss_sum
|
| 615 |
+
decoder_token_count = decoder_token_count + step_token_count
|
| 616 |
+
|
| 617 |
+
if tbptt_steps is not None:
|
| 618 |
+
steps_since_detach += 1
|
| 619 |
+
if steps_since_detach >= tbptt_steps:
|
| 620 |
+
z_state = z_state.detach()
|
| 621 |
+
state_mask = state_mask.detach()
|
| 622 |
+
steps_since_detach = 0
|
| 623 |
+
|
| 624 |
+
if gate_logits_history:
|
| 625 |
+
stacked_gate_logits = torch.stack(gate_logits_history, dim=1)
|
| 626 |
+
stacked_gate_targets = torch.stack(gate_target_history, dim=1)
|
| 627 |
+
stacked_gate_loss_mask = torch.stack(gate_loss_mask_history, dim=1)
|
| 628 |
+
stacked_observation_roles = torch.stack(observation_role_history, dim=1)
|
| 629 |
+
else:
|
| 630 |
+
stacked_gate_logits = torch.zeros((batch_size, 0), device=device)
|
| 631 |
+
stacked_gate_targets = torch.zeros((batch_size, 0), device=device)
|
| 632 |
+
stacked_gate_loss_mask = torch.zeros((batch_size, 0), device=device)
|
| 633 |
+
stacked_observation_roles = torch.zeros((batch_size, 0), device=device, dtype=torch.long)
|
| 634 |
+
|
| 635 |
+
gate_ranking_loss = torch.zeros((), device=device)
|
| 636 |
+
gate_ranking_loss_sum = torch.zeros((), device=device)
|
| 637 |
+
gate_ranking_pair_count = torch.zeros((), device=device)
|
| 638 |
+
if freeze_gate_head_to_targets:
|
| 639 |
+
gate_loss = torch.zeros((), device=device)
|
| 640 |
+
gate_loss_sum = torch.zeros((), device=device)
|
| 641 |
+
gate_count = torch.zeros((), device=device)
|
| 642 |
+
hazard_event_count = torch.zeros((), device=device)
|
| 643 |
+
hazard_censor_count = torch.zeros((), device=device)
|
| 644 |
+
elif use_hazard_gate_loss and gate_logits_history:
|
| 645 |
+
gate_loss, hazard_metrics = compute_distance_aware_hazard_gate_loss(
|
| 646 |
+
gate_logits=stacked_gate_logits,
|
| 647 |
+
gate_targets=stacked_gate_targets,
|
| 648 |
+
gate_loss_mask=stacked_gate_loss_mask,
|
| 649 |
+
training_cfg=training_cfg,
|
| 650 |
+
)
|
| 651 |
+
gate_loss_sum = hazard_metrics["gate_loss_sum"]
|
| 652 |
+
gate_count = hazard_metrics["gate_count"]
|
| 653 |
+
hazard_event_count = hazard_metrics["hazard_event_count"]
|
| 654 |
+
hazard_censor_count = hazard_metrics["hazard_censor_count"]
|
| 655 |
+
elif use_soft_bce_gate_loss and gate_logits_history:
|
| 656 |
+
gate_loss, soft_bce_metrics = compute_soft_bce_gate_loss(
|
| 657 |
+
gate_logits=stacked_gate_logits,
|
| 658 |
+
hard_gate_targets=stacked_gate_targets,
|
| 659 |
+
gate_loss_mask=stacked_gate_loss_mask,
|
| 660 |
+
observation_roles=stacked_observation_roles,
|
| 661 |
+
positive_weight=pos_weight,
|
| 662 |
+
training_cfg=training_cfg,
|
| 663 |
+
)
|
| 664 |
+
gate_loss_sum = soft_bce_metrics["gate_loss_sum"]
|
| 665 |
+
gate_count = soft_bce_metrics["gate_count"]
|
| 666 |
+
soft_target_mass = soft_bce_metrics["soft_target_mass"]
|
| 667 |
+
soft_target_positive_count = soft_bce_metrics["soft_target_positive_count"]
|
| 668 |
+
else:
|
| 669 |
+
gate_loss = gate_loss_sum / gate_count.clamp_min(1.0)
|
| 670 |
+
|
| 671 |
+
if (
|
| 672 |
+
not use_hazard_gate_loss
|
| 673 |
+
and bool(training_cfg.get("use_ranked_gate_objective", False))
|
| 674 |
+
and gate_logits_history
|
| 675 |
+
and not freeze_gate_head_to_targets
|
| 676 |
+
):
|
| 677 |
+
gate_ranking_loss_sum, gate_ranking_pair_count = compute_gate_ranking_loss(
|
| 678 |
+
gate_logits=stacked_gate_logits,
|
| 679 |
+
gate_targets=stacked_gate_targets,
|
| 680 |
+
gate_loss_mask=stacked_gate_loss_mask,
|
| 681 |
+
ranking_margin=float(training_cfg.get("gate_ranking_margin", 0.5)),
|
| 682 |
+
ranking_lookback=int(training_cfg.get("gate_ranking_lookback", 64)),
|
| 683 |
+
)
|
| 684 |
+
gate_ranking_loss = gate_ranking_loss_sum / gate_ranking_pair_count.clamp_min(1.0)
|
| 685 |
+
gate_loss = (
|
| 686 |
+
float(training_cfg.get("gate_bce_weight", 1.0)) * gate_loss
|
| 687 |
+
+ float(training_cfg.get("gate_ranking_weight", 1.0)) * gate_ranking_loss
|
| 688 |
+
)
|
| 689 |
+
|
| 690 |
+
decoder_loss = decoder_loss_sum / decoder_token_count.clamp_min(1.0)
|
| 691 |
+
state_delta_loss = state_penalty_sum / state_penalty_count.clamp_min(1.0)
|
| 692 |
+
total_loss = (
|
| 693 |
+
float(training_cfg["gate_loss_weight"]) * gate_loss
|
| 694 |
+
+ float(training_cfg["decoder_loss_weight"]) * decoder_loss
|
| 695 |
+
+ state_delta_weight * state_delta_loss
|
| 696 |
+
)
|
| 697 |
+
|
| 698 |
+
metrics = {
|
| 699 |
+
"loss_total": total_loss.detach(),
|
| 700 |
+
"gate_loss_sum": gate_loss_sum.detach(),
|
| 701 |
+
"gate_count": gate_count.detach(),
|
| 702 |
+
"decoder_loss_sum": decoder_loss_sum.detach(),
|
| 703 |
+
"decoder_token_count": decoder_token_count.detach(),
|
| 704 |
+
"state_penalty_sum": state_penalty_sum.detach(),
|
| 705 |
+
"state_penalty_count": state_penalty_count.detach(),
|
| 706 |
+
"gate_tp": tp.detach(),
|
| 707 |
+
"gate_fp": fp.detach(),
|
| 708 |
+
"gate_fn": fn.detach(),
|
| 709 |
+
"gate_correct": correct.detach(),
|
| 710 |
+
"gate_eval_count": counted.detach(),
|
| 711 |
+
"gate_target_positive_count": target_positive_count.detach(),
|
| 712 |
+
"gate_pred_positive_count": predicted_positive_count.detach(),
|
| 713 |
+
"gate_ranking_loss_sum": gate_ranking_loss_sum.detach(),
|
| 714 |
+
"gate_ranking_pair_count": gate_ranking_pair_count.detach(),
|
| 715 |
+
"hazard_event_count": hazard_event_count.detach(),
|
| 716 |
+
"hazard_censor_count": hazard_censor_count.detach(),
|
| 717 |
+
"soft_target_mass": soft_target_mass.detach(),
|
| 718 |
+
"soft_target_positive_count": soft_target_positive_count.detach(),
|
| 719 |
+
"active_ticks": batch["tick_mask"].float().sum().detach(),
|
| 720 |
+
"gate_positive_weight": pos_weight.detach(),
|
| 721 |
+
"metrics_count": torch.ones((), device=device),
|
| 722 |
+
}
|
| 723 |
+
return total_loss, metrics, z_state, state_mask
|
| 724 |
+
|
| 725 |
+
|
| 726 |
+
def reduce_metrics(
|
| 727 |
+
accelerator: Accelerator,
|
| 728 |
+
metric_list: list[dict[str, torch.Tensor]],
|
| 729 |
+
training_cfg: dict[str, Any],
|
| 730 |
+
) -> dict[str, float]:
|
| 731 |
+
totals: dict[str, torch.Tensor] = {}
|
| 732 |
+
for metrics in metric_list:
|
| 733 |
+
for key, value in metrics.items():
|
| 734 |
+
totals[key] = totals.get(key, torch.zeros_like(value)) + value
|
| 735 |
+
|
| 736 |
+
reduced = {key: accelerator.reduce(value, reduction="sum") for key, value in totals.items()}
|
| 737 |
+
|
| 738 |
+
gate_loss_mode = str(training_cfg.get("gate_loss_mode", "hazard")).strip().lower()
|
| 739 |
+
gate_loss = reduced["gate_loss_sum"] / reduced["gate_count"].clamp_min(1.0)
|
| 740 |
+
gate_ranking_loss = reduced["gate_ranking_loss_sum"] / reduced["gate_ranking_pair_count"].clamp_min(1.0)
|
| 741 |
+
decoder_loss = reduced["decoder_loss_sum"] / reduced["decoder_token_count"].clamp_min(1.0)
|
| 742 |
+
state_delta_loss = reduced["state_penalty_sum"] / reduced["state_penalty_count"].clamp_min(1.0)
|
| 743 |
+
gate_precision = reduced["gate_tp"] / (reduced["gate_tp"] + reduced["gate_fp"]).clamp_min(1.0)
|
| 744 |
+
gate_recall = reduced["gate_tp"] / (reduced["gate_tp"] + reduced["gate_fn"]).clamp_min(1.0)
|
| 745 |
+
gate_f1 = 2 * gate_precision * gate_recall / (gate_precision + gate_recall).clamp_min(1e-8)
|
| 746 |
+
if gate_loss_mode != "hazard" and bool(training_cfg.get("use_ranked_gate_objective", False)):
|
| 747 |
+
gate_loss = (
|
| 748 |
+
float(training_cfg.get("gate_bce_weight", 1.0)) * gate_loss
|
| 749 |
+
+ float(training_cfg.get("gate_ranking_weight", 1.0)) * gate_ranking_loss
|
| 750 |
+
)
|
| 751 |
+
total_loss = (
|
| 752 |
+
float(training_cfg["gate_loss_weight"]) * gate_loss
|
| 753 |
+
+ float(training_cfg["decoder_loss_weight"]) * decoder_loss
|
| 754 |
+
+ float(training_cfg["state_delta_weight"]) * state_delta_loss
|
| 755 |
+
)
|
| 756 |
+
|
| 757 |
+
return {
|
| 758 |
+
"loss_total": float(total_loss),
|
| 759 |
+
"loss_gate": float(gate_loss),
|
| 760 |
+
"loss_gate_ranking": float(gate_ranking_loss),
|
| 761 |
+
"loss_decoder": float(decoder_loss),
|
| 762 |
+
"loss_state_delta": float(state_delta_loss),
|
| 763 |
+
"gate_accuracy": float(reduced["gate_correct"] / reduced["gate_eval_count"].clamp_min(1.0)),
|
| 764 |
+
"gate_precision": float(gate_precision),
|
| 765 |
+
"gate_recall": float(gate_recall),
|
| 766 |
+
"gate_f1": float(gate_f1),
|
| 767 |
+
"gate_target_positive_count": float(reduced["gate_target_positive_count"]),
|
| 768 |
+
"gate_pred_positive_count": float(reduced["gate_pred_positive_count"]),
|
| 769 |
+
"gate_positive_weight": float(reduced["gate_positive_weight"] / reduced["metrics_count"].clamp_min(1.0)),
|
| 770 |
+
"hazard_event_count": float(reduced["hazard_event_count"]),
|
| 771 |
+
"hazard_censor_count": float(reduced["hazard_censor_count"]),
|
| 772 |
+
"soft_target_mass": float(reduced["soft_target_mass"]),
|
| 773 |
+
"soft_target_positive_count": float(reduced["soft_target_positive_count"]),
|
| 774 |
+
"decoder_token_count": float(reduced["decoder_token_count"]),
|
| 775 |
+
"active_ticks": float(reduced["active_ticks"]),
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
|
| 779 |
+
@torch.no_grad()
|
| 780 |
+
def evaluate(
|
| 781 |
+
accelerator: Accelerator,
|
| 782 |
+
model: ThoughtLoopT5Gemma,
|
| 783 |
+
dataloader: DataLoader,
|
| 784 |
+
training_cfg: dict[str, Any],
|
| 785 |
+
rollout_cfg: dict[str, Any],
|
| 786 |
+
tokenizer_pad_token_id: int,
|
| 787 |
+
max_batches: int | None = None,
|
| 788 |
+
) -> dict[str, float]:
|
| 789 |
+
model.eval()
|
| 790 |
+
all_metrics: list[dict[str, torch.Tensor]] = []
|
| 791 |
+
chunk_ticks = int(rollout_cfg["chunk_ticks"])
|
| 792 |
+
tick_seconds = float(rollout_cfg["tick_seconds"])
|
| 793 |
+
|
| 794 |
+
for batch_index, conversations in enumerate(dataloader):
|
| 795 |
+
if max_batches is not None and batch_index >= max_batches:
|
| 796 |
+
break
|
| 797 |
+
|
| 798 |
+
batch_size = len(conversations)
|
| 799 |
+
z_state = model.initial_state(batch_size=batch_size, device=accelerator.device)
|
| 800 |
+
state_mask = model.initial_state_mask(batch_size=batch_size, device=accelerator.device)
|
| 801 |
+
max_chunks = max(int(conversation["chunk_count"]) for conversation in conversations)
|
| 802 |
+
for chunk_index in range(max_chunks):
|
| 803 |
+
chunk_batch = build_chunk_batch(
|
| 804 |
+
conversations=conversations,
|
| 805 |
+
chunk_index=chunk_index,
|
| 806 |
+
pad_token_id=tokenizer_pad_token_id,
|
| 807 |
+
chunk_ticks=chunk_ticks,
|
| 808 |
+
tick_seconds=tick_seconds,
|
| 809 |
+
)
|
| 810 |
+
if chunk_batch is None:
|
| 811 |
+
break
|
| 812 |
+
|
| 813 |
+
chunk_batch = move_chunk_batch_to_device(chunk_batch, accelerator.device)
|
| 814 |
+
_, metrics, z_state, state_mask = compute_chunk_losses(
|
| 815 |
+
model=model,
|
| 816 |
+
batch=chunk_batch,
|
| 817 |
+
training_cfg=training_cfg,
|
| 818 |
+
z_state=z_state,
|
| 819 |
+
state_mask=state_mask,
|
| 820 |
+
tbptt_steps=None,
|
| 821 |
+
freeze_gate_head_to_targets=False,
|
| 822 |
+
)
|
| 823 |
+
z_state = z_state.detach()
|
| 824 |
+
state_mask = state_mask.detach()
|
| 825 |
+
all_metrics.append(metrics)
|
| 826 |
+
|
| 827 |
+
if not all_metrics:
|
| 828 |
+
model.train()
|
| 829 |
+
return {}
|
| 830 |
+
|
| 831 |
+
reduced = reduce_metrics(accelerator, all_metrics, training_cfg)
|
| 832 |
+
model.train()
|
| 833 |
+
return reduced
|
| 834 |
+
|
| 835 |
+
|
| 836 |
+
def copy_supporting_files(output_dir: Path, config_path: Path) -> None:
|
| 837 |
+
for filename in ("config.py", "data.py", "model.py", "inference.py", "train.py"):
|
| 838 |
+
shutil.copy2(SCRIPT_DIR / filename, output_dir / filename)
|
| 839 |
+
shutil.copy2(config_path, output_dir / config_path.name)
|
| 840 |
+
|
| 841 |
+
|
| 842 |
+
def build_model_card(config: dict[str, Any], best_metrics: dict[str, float]) -> str:
|
| 843 |
+
return "\n".join(
|
| 844 |
+
[
|
| 845 |
+
"# Samantha Thought-Loop SFT",
|
| 846 |
+
"",
|
| 847 |
+
f"- Base model: `{config['model']['base_model_name']}`",
|
| 848 |
+
f"- Cleaned dataset repo: `{config['dataset'].get('cleaned_repo_id')}`",
|
| 849 |
+
f"- Raw dataset repo: `{config['dataset'].get('raw_repo_id')}`",
|
| 850 |
+
f"- Latent slots: `{config['model']['z_slots']}`",
|
| 851 |
+
f"- Fixed tick seconds: `{config['rollout']['tick_seconds']}`",
|
| 852 |
+
f"- Chunk ticks: `{config['rollout']['chunk_ticks']}`",
|
| 853 |
+
f"- Max horizon ticks: `{config['rollout'].get('max_horizon_ticks', 36000)}`",
|
| 854 |
+
f"- Explicit time features: `{config['model'].get('use_explicit_time_features', False)}`",
|
| 855 |
+
f"- Gate loss mode: `{config['training'].get('gate_loss_mode', 'hazard')}`",
|
| 856 |
+
f"- Hazard soft target window: `{config['training'].get('hazard_soft_target_window_ticks', 16)}`",
|
| 857 |
+
f"- Hazard soft target tau ticks: `{config['training'].get('hazard_soft_target_tau_ticks', 4.0)}`",
|
| 858 |
+
f"- Hazard censor weight: `{config['training'].get('hazard_censor_weight', 0.25)}`",
|
| 859 |
+
f"- TBPTT by bucket: `{config['training'].get('tbptt_steps_by_bucket', {})}`",
|
| 860 |
+
"",
|
| 861 |
+
"## Best Validation Metrics",
|
| 862 |
+
"",
|
| 863 |
+
*[f"- `{key}`: `{value:.6f}`" for key, value in sorted(best_metrics.items())],
|
| 864 |
+
"",
|
| 865 |
+
]
|
| 866 |
+
)
|
| 867 |
+
|
| 868 |
+
|
| 869 |
+
def export_model(
|
| 870 |
+
accelerator: Accelerator,
|
| 871 |
+
model: ThoughtLoopT5Gemma,
|
| 872 |
+
output_dir: Path,
|
| 873 |
+
config: dict[str, Any],
|
| 874 |
+
config_path: Path,
|
| 875 |
+
best_metrics: dict[str, float],
|
| 876 |
+
) -> None:
|
| 877 |
+
if not accelerator.is_main_process:
|
| 878 |
+
return
|
| 879 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 880 |
+
unwrapped = accelerator.unwrap_model(model)
|
| 881 |
+
unwrapped.save_pretrained(output_dir)
|
| 882 |
+
copy_supporting_files(output_dir, config_path)
|
| 883 |
+
(output_dir / "README.md").write_text(build_model_card(config, best_metrics), encoding="utf-8")
|
| 884 |
+
save_json(output_dir / "best_metrics.json", best_metrics)
|
| 885 |
+
|
| 886 |
+
|
| 887 |
+
def maybe_push_to_hub(config: dict[str, Any], export_dir: Path) -> None:
|
| 888 |
+
model_repo_id = config["hub"].get("model_repo_id")
|
| 889 |
+
if not model_repo_id:
|
| 890 |
+
return
|
| 891 |
+
api = HfApi()
|
| 892 |
+
api.create_repo(repo_id=model_repo_id, repo_type="model", exist_ok=True, private=bool(config["hub"].get("private")))
|
| 893 |
+
api.upload_folder(folder_path=str(export_dir), repo_id=model_repo_id, repo_type="model")
|
| 894 |
+
|
| 895 |
+
|
| 896 |
+
def maybe_push_checkpoint_to_hub(
|
| 897 |
+
accelerator: Accelerator,
|
| 898 |
+
config: dict[str, Any],
|
| 899 |
+
checkpoint_dir: Path,
|
| 900 |
+
) -> None:
|
| 901 |
+
model_repo_id = config["hub"].get("model_repo_id")
|
| 902 |
+
if not accelerator.is_main_process or not model_repo_id:
|
| 903 |
+
return
|
| 904 |
+
if not bool(config["hub"].get("push_checkpoints", True)):
|
| 905 |
+
return
|
| 906 |
+
|
| 907 |
+
run_name = str(config["wandb"].get("run_name", "run"))
|
| 908 |
+
checkpoint_prefix = str(config["hub"].get("checkpoint_path_prefix", "checkpoints")).strip("/")
|
| 909 |
+
path_parts = [part for part in (checkpoint_prefix, SCRIPT_DIR.name, run_name, checkpoint_dir.name) if part]
|
| 910 |
+
path_in_repo = "/".join(path_parts)
|
| 911 |
+
|
| 912 |
+
api = HfApi()
|
| 913 |
+
api.create_repo(repo_id=model_repo_id, repo_type="model", exist_ok=True, private=bool(config["hub"].get("private")))
|
| 914 |
+
api.upload_folder(
|
| 915 |
+
folder_path=str(checkpoint_dir),
|
| 916 |
+
repo_id=model_repo_id,
|
| 917 |
+
repo_type="model",
|
| 918 |
+
path_in_repo=path_in_repo,
|
| 919 |
+
commit_message=f"Add {SCRIPT_DIR.name} {checkpoint_dir.name}",
|
| 920 |
+
)
|
| 921 |
+
|
| 922 |
+
|
| 923 |
+
def read_json(path: Path) -> dict[str, Any]:
|
| 924 |
+
if not path.exists():
|
| 925 |
+
return {}
|
| 926 |
+
with path.open("r", encoding="utf-8") as handle:
|
| 927 |
+
payload = json.load(handle)
|
| 928 |
+
if not isinstance(payload, dict):
|
| 929 |
+
raise ValueError(f"Expected a JSON object in {path}.")
|
| 930 |
+
return payload
|
| 931 |
+
|
| 932 |
+
|
| 933 |
+
def checkpoint_global_step(checkpoint_dir: Path) -> int | None:
|
| 934 |
+
state = read_json(checkpoint_dir / "trainer_state.json")
|
| 935 |
+
if "global_step" in state:
|
| 936 |
+
return int(state["global_step"])
|
| 937 |
+
|
| 938 |
+
prefix = "checkpoint-"
|
| 939 |
+
if checkpoint_dir.name.startswith(prefix):
|
| 940 |
+
suffix = checkpoint_dir.name[len(prefix) :]
|
| 941 |
+
if suffix.isdigit():
|
| 942 |
+
return int(suffix)
|
| 943 |
+
return None
|
| 944 |
+
|
| 945 |
+
|
| 946 |
+
def find_latest_checkpoint(run_dir: Path) -> Path | None:
|
| 947 |
+
checkpoints = []
|
| 948 |
+
for candidate in run_dir.glob("checkpoint-*"):
|
| 949 |
+
if not candidate.is_dir():
|
| 950 |
+
continue
|
| 951 |
+
step = checkpoint_global_step(candidate)
|
| 952 |
+
if step is not None:
|
| 953 |
+
checkpoints.append((step, candidate))
|
| 954 |
+
if not checkpoints:
|
| 955 |
+
return None
|
| 956 |
+
return max(checkpoints, key=lambda item: item[0])[1]
|
| 957 |
+
|
| 958 |
+
|
| 959 |
+
def resolve_resume_checkpoint(requested: str | None, run_dir: Path) -> Path | None:
|
| 960 |
+
if requested is None:
|
| 961 |
+
return None
|
| 962 |
+
|
| 963 |
+
value = str(requested).strip()
|
| 964 |
+
if not value:
|
| 965 |
+
return None
|
| 966 |
+
|
| 967 |
+
if value.lower() in {"1", "true", "yes", "auto", "latest"}:
|
| 968 |
+
checkpoint_dir = find_latest_checkpoint(run_dir)
|
| 969 |
+
if checkpoint_dir is None:
|
| 970 |
+
raise FileNotFoundError(f"No checkpoint-* directory found under {run_dir}.")
|
| 971 |
+
return checkpoint_dir
|
| 972 |
+
|
| 973 |
+
checkpoint_dir = Path(value).expanduser()
|
| 974 |
+
if not checkpoint_dir.is_absolute():
|
| 975 |
+
checkpoint_dir = SCRIPT_DIR / checkpoint_dir
|
| 976 |
+
checkpoint_dir = checkpoint_dir.resolve()
|
| 977 |
+
if not checkpoint_dir.is_dir():
|
| 978 |
+
raise FileNotFoundError(f"Resume checkpoint does not exist: {checkpoint_dir}")
|
| 979 |
+
return checkpoint_dir
|
| 980 |
+
|
| 981 |
+
|
| 982 |
+
def parse_wandb_run_id_from_dir(path: Path) -> str | None:
|
| 983 |
+
name = path.resolve(strict=False).name if path.is_symlink() else path.name
|
| 984 |
+
for prefix in ("offline-run-", "run-"):
|
| 985 |
+
if name.startswith(prefix):
|
| 986 |
+
parts = name.split("-")
|
| 987 |
+
if parts:
|
| 988 |
+
return parts[-1]
|
| 989 |
+
return None
|
| 990 |
+
|
| 991 |
+
|
| 992 |
+
def discover_local_wandb_run_id(run_name: str) -> str | None:
|
| 993 |
+
wandb_dir = SCRIPT_DIR / "wandb"
|
| 994 |
+
if not wandb_dir.exists():
|
| 995 |
+
return None
|
| 996 |
+
|
| 997 |
+
candidates: list[Path] = []
|
| 998 |
+
latest = wandb_dir / "latest-run"
|
| 999 |
+
if latest.exists() or latest.is_symlink():
|
| 1000 |
+
candidates.append(latest)
|
| 1001 |
+
candidates.extend(sorted(wandb_dir.glob("*run-*"), key=lambda path: path.stat().st_mtime, reverse=True))
|
| 1002 |
+
|
| 1003 |
+
for candidate in candidates:
|
| 1004 |
+
run_id = parse_wandb_run_id_from_dir(candidate)
|
| 1005 |
+
if not run_id:
|
| 1006 |
+
continue
|
| 1007 |
+
resolved = candidate.resolve(strict=False)
|
| 1008 |
+
config_path = resolved / "files" / "config.yaml"
|
| 1009 |
+
debug_log_path = resolved / "logs" / "debug.log"
|
| 1010 |
+
if config_path.exists() and run_name in config_path.read_text(encoding="utf-8", errors="ignore"):
|
| 1011 |
+
return run_id
|
| 1012 |
+
if debug_log_path.exists() and run_name in debug_log_path.read_text(encoding="utf-8", errors="ignore"):
|
| 1013 |
+
return run_id
|
| 1014 |
+
return None
|
| 1015 |
+
|
| 1016 |
+
|
| 1017 |
+
def resolve_wandb_run_id(args: argparse.Namespace, config: dict[str, Any], run_dir: Path) -> str:
|
| 1018 |
+
run_id_path = run_dir / "wandb_run_id.txt"
|
| 1019 |
+
explicit = args.wandb_run_id or os.environ.get("WANDB_RUN_ID") or config["wandb"].get("run_id")
|
| 1020 |
+
if explicit:
|
| 1021 |
+
run_id = str(explicit).strip()
|
| 1022 |
+
elif run_id_path.exists():
|
| 1023 |
+
run_id = run_id_path.read_text(encoding="utf-8").strip()
|
| 1024 |
+
else:
|
| 1025 |
+
run_id = discover_local_wandb_run_id(str(config["wandb"].get("run_name", ""))) or secrets.token_hex(4)
|
| 1026 |
+
|
| 1027 |
+
if not run_id:
|
| 1028 |
+
run_id = secrets.token_hex(4)
|
| 1029 |
+
run_id_path.write_text(run_id + "\n", encoding="utf-8")
|
| 1030 |
+
return run_id
|
| 1031 |
+
|
| 1032 |
+
|
| 1033 |
+
def save_trainer_state(
|
| 1034 |
+
checkpoint_dir: Path,
|
| 1035 |
+
*,
|
| 1036 |
+
global_step: int,
|
| 1037 |
+
epoch: int,
|
| 1038 |
+
next_batch_index: int,
|
| 1039 |
+
best_validation_loss: float,
|
| 1040 |
+
best_metrics: dict[str, float],
|
| 1041 |
+
wandb_run_id: str | None,
|
| 1042 |
+
) -> None:
|
| 1043 |
+
payload: dict[str, Any] = {
|
| 1044 |
+
"global_step": int(global_step),
|
| 1045 |
+
"epoch": int(epoch),
|
| 1046 |
+
"next_batch_index": int(next_batch_index),
|
| 1047 |
+
"best_validation_loss": best_validation_loss if math.isfinite(best_validation_loss) else None,
|
| 1048 |
+
"best_metrics": best_metrics,
|
| 1049 |
+
"wandb_run_id": wandb_run_id,
|
| 1050 |
+
}
|
| 1051 |
+
save_json(checkpoint_dir / "trainer_state.json", payload)
|
| 1052 |
+
|
| 1053 |
+
|
| 1054 |
+
def load_resume_progress(
|
| 1055 |
+
checkpoint_dir: Path,
|
| 1056 |
+
*,
|
| 1057 |
+
total_chunk_microsteps: int,
|
| 1058 |
+
gradient_accumulation_steps: int,
|
| 1059 |
+
num_train_epochs: int,
|
| 1060 |
+
) -> tuple[int, int, int, dict[str, Any]]:
|
| 1061 |
+
state = read_json(checkpoint_dir / "trainer_state.json")
|
| 1062 |
+
global_step = int(state.get("global_step") or checkpoint_global_step(checkpoint_dir) or 0)
|
| 1063 |
+
if "epoch" in state and "next_batch_index" in state:
|
| 1064 |
+
start_epoch = int(state["epoch"])
|
| 1065 |
+
batches_to_skip = int(state["next_batch_index"])
|
| 1066 |
+
else:
|
| 1067 |
+
updates_per_epoch = max(1, math.ceil(total_chunk_microsteps / max(gradient_accumulation_steps, 1)))
|
| 1068 |
+
start_epoch = min(global_step // updates_per_epoch, max(num_train_epochs - 1, 0))
|
| 1069 |
+
updates_into_epoch = global_step - start_epoch * updates_per_epoch
|
| 1070 |
+
batches_to_skip = updates_into_epoch * max(gradient_accumulation_steps, 1)
|
| 1071 |
+
|
| 1072 |
+
while batches_to_skip >= total_chunk_microsteps and start_epoch < num_train_epochs:
|
| 1073 |
+
batches_to_skip -= total_chunk_microsteps
|
| 1074 |
+
start_epoch += 1
|
| 1075 |
+
|
| 1076 |
+
return global_step, start_epoch, batches_to_skip, state
|
| 1077 |
+
|
| 1078 |
+
|
| 1079 |
+
def load_best_metrics(export_dir: Path, resume_state: dict[str, Any]) -> tuple[float, dict[str, float]]:
|
| 1080 |
+
best_metrics = resume_state.get("best_metrics")
|
| 1081 |
+
if not isinstance(best_metrics, dict):
|
| 1082 |
+
best_metrics = read_json(export_dir / "best_metrics.json")
|
| 1083 |
+
|
| 1084 |
+
loss = resume_state.get("best_validation_loss")
|
| 1085 |
+
if loss is None and isinstance(best_metrics, dict):
|
| 1086 |
+
loss = best_metrics.get("loss_total")
|
| 1087 |
+
best_validation_loss = float(loss) if loss is not None else float("inf")
|
| 1088 |
+
return best_validation_loss, {str(key): float(value) for key, value in best_metrics.items()}
|
| 1089 |
+
|
| 1090 |
+
|
| 1091 |
+
def main() -> None:
|
| 1092 |
+
args = parse_args()
|
| 1093 |
+
config_path = Path(args.config)
|
| 1094 |
+
config = load_config(config_path)
|
| 1095 |
+
training_cfg = config["training"]
|
| 1096 |
+
rollout_cfg = config["rollout"]
|
| 1097 |
+
|
| 1098 |
+
set_seed(int(training_cfg["seed"]))
|
| 1099 |
+
wandb_enabled = bool(config["wandb"].get("enabled", True) and wandb is not None)
|
| 1100 |
+
|
| 1101 |
+
accelerator = Accelerator(
|
| 1102 |
+
gradient_accumulation_steps=int(training_cfg["gradient_accumulation_steps"]),
|
| 1103 |
+
mixed_precision=training_cfg.get("mixed_precision", "bf16"),
|
| 1104 |
+
log_with="wandb" if wandb_enabled else None,
|
| 1105 |
+
project_dir=config["paths"]["run_root"],
|
| 1106 |
+
)
|
| 1107 |
+
|
| 1108 |
+
run_root = ensure_dir(config["paths"]["run_root"])
|
| 1109 |
+
export_root = ensure_dir(config["paths"]["export_root"])
|
| 1110 |
+
run_name = config["wandb"]["run_name"]
|
| 1111 |
+
run_dir = ensure_dir(run_root / run_name)
|
| 1112 |
+
export_dir = ensure_dir(export_root / run_name)
|
| 1113 |
+
resume_request = args.resume_from_checkpoint or training_cfg.get("resume_from_checkpoint")
|
| 1114 |
+
resume_checkpoint = resolve_resume_checkpoint(resume_request, run_dir)
|
| 1115 |
+
wandb_run_id = resolve_wandb_run_id(args, config, run_dir) if wandb_enabled else None
|
| 1116 |
+
|
| 1117 |
+
model = build_initial_model(config)
|
| 1118 |
+
if model.use_explicit_time_features:
|
| 1119 |
+
raise ValueError("This trainer expects model.use_explicit_time_features=false for fixed-cadence training.")
|
| 1120 |
+
tokenizer = model.tokenizer
|
| 1121 |
+
|
| 1122 |
+
train_dataset = ThoughtLoopConversationDataset(config=config, tokenizer=tokenizer, split="train")
|
| 1123 |
+
validation_dataset = ThoughtLoopConversationDataset(config=config, tokenizer=tokenizer, split="validation")
|
| 1124 |
+
sort_dataset_by_length = bool(training_cfg.get("sort_dataset_by_length", False))
|
| 1125 |
+
|
| 1126 |
+
ensure_single_chunk_dataset(train_dataset, "train")
|
| 1127 |
+
ensure_single_chunk_dataset(validation_dataset, "validation")
|
| 1128 |
+
maybe_sort_dataset_by_length(train_dataset, enabled=sort_dataset_by_length)
|
| 1129 |
+
maybe_sort_dataset_by_length(validation_dataset, enabled=sort_dataset_by_length)
|
| 1130 |
+
|
| 1131 |
+
train_loader = DataLoader(
|
| 1132 |
+
train_dataset,
|
| 1133 |
+
batch_size=int(training_cfg["micro_batch_size"]),
|
| 1134 |
+
shuffle=bool(training_cfg.get("shuffle_train", True)) and not sort_dataset_by_length,
|
| 1135 |
+
num_workers=int(training_cfg["num_workers"]),
|
| 1136 |
+
pin_memory=True,
|
| 1137 |
+
persistent_workers=bool(int(training_cfg["num_workers"]) > 0),
|
| 1138 |
+
collate_fn=identity_collate,
|
| 1139 |
+
)
|
| 1140 |
+
validation_loader = DataLoader(
|
| 1141 |
+
validation_dataset,
|
| 1142 |
+
batch_size=int(training_cfg["eval_batch_size"]),
|
| 1143 |
+
shuffle=False,
|
| 1144 |
+
num_workers=int(training_cfg["num_workers"]),
|
| 1145 |
+
pin_memory=True,
|
| 1146 |
+
persistent_workers=bool(int(training_cfg["num_workers"]) > 0),
|
| 1147 |
+
collate_fn=identity_collate,
|
| 1148 |
+
)
|
| 1149 |
+
|
| 1150 |
+
optimizer = build_optimizer(model, training_cfg)
|
| 1151 |
+
total_chunk_microsteps = len(train_loader)
|
| 1152 |
+
total_update_steps = max(
|
| 1153 |
+
1,
|
| 1154 |
+
math.ceil(total_chunk_microsteps / max(int(training_cfg["gradient_accumulation_steps"]), 1))
|
| 1155 |
+
* int(training_cfg["num_train_epochs"]),
|
| 1156 |
+
)
|
| 1157 |
+
warmup_steps = max(1, int(total_update_steps * float(training_cfg["warmup_ratio"])))
|
| 1158 |
+
scheduler = get_cosine_schedule_with_warmup(
|
| 1159 |
+
optimizer=optimizer,
|
| 1160 |
+
num_warmup_steps=warmup_steps,
|
| 1161 |
+
num_training_steps=max(1, total_update_steps),
|
| 1162 |
+
)
|
| 1163 |
+
|
| 1164 |
+
model, optimizer, train_loader, validation_loader, scheduler = accelerator.prepare(
|
| 1165 |
+
model, optimizer, train_loader, validation_loader, scheduler
|
| 1166 |
+
)
|
| 1167 |
+
|
| 1168 |
+
resume_state: dict[str, Any] = {}
|
| 1169 |
+
global_step = 0
|
| 1170 |
+
start_epoch = 0
|
| 1171 |
+
batches_to_skip = 0
|
| 1172 |
+
if resume_checkpoint is not None:
|
| 1173 |
+
accelerator.print(f"Loading local checkpoint from {resume_checkpoint}")
|
| 1174 |
+
accelerator.load_state(str(resume_checkpoint))
|
| 1175 |
+
global_step, start_epoch, batches_to_skip, resume_state = load_resume_progress(
|
| 1176 |
+
resume_checkpoint,
|
| 1177 |
+
total_chunk_microsteps=total_chunk_microsteps,
|
| 1178 |
+
gradient_accumulation_steps=int(training_cfg["gradient_accumulation_steps"]),
|
| 1179 |
+
num_train_epochs=int(training_cfg["num_train_epochs"]),
|
| 1180 |
+
)
|
| 1181 |
+
accelerator.print(
|
| 1182 |
+
f"Resuming at global_step={global_step}, epoch={start_epoch + 1}, "
|
| 1183 |
+
f"skipping {batches_to_skip} batch(es)."
|
| 1184 |
+
)
|
| 1185 |
+
|
| 1186 |
+
if accelerator.is_main_process and wandb_enabled:
|
| 1187 |
+
accelerator.print(f"W&B run id: {wandb_run_id}")
|
| 1188 |
+
accelerator.init_trackers(
|
| 1189 |
+
project_name=config["wandb"]["project"],
|
| 1190 |
+
config=flatten_for_wandb(config),
|
| 1191 |
+
init_kwargs={
|
| 1192 |
+
"wandb": {
|
| 1193 |
+
"name": run_name,
|
| 1194 |
+
"id": wandb_run_id,
|
| 1195 |
+
"resume": str(config["wandb"].get("resume", "allow")),
|
| 1196 |
+
}
|
| 1197 |
+
},
|
| 1198 |
+
)
|
| 1199 |
+
|
| 1200 |
+
unwrapped_model = accelerator.unwrap_model(model)
|
| 1201 |
+
current_decoder_fraction = resolve_decoder_trainable_fraction(training_cfg, global_step, total_update_steps)
|
| 1202 |
+
if current_decoder_fraction is None:
|
| 1203 |
+
current_decoder_fraction = 1.0
|
| 1204 |
+
current_decoder_trainable_layers = unwrapped_model.set_decoder_trainable_fraction(current_decoder_fraction)
|
| 1205 |
+
|
| 1206 |
+
current_gate_head_frozen = should_freeze_gate_head(training_cfg, global_step, total_update_steps)
|
| 1207 |
+
unwrapped_model.set_gate_trainable(not current_gate_head_frozen)
|
| 1208 |
+
|
| 1209 |
+
progress_bar = tqdm(
|
| 1210 |
+
total=total_update_steps,
|
| 1211 |
+
initial=global_step,
|
| 1212 |
+
disable=not accelerator.is_local_main_process,
|
| 1213 |
+
desc="Training",
|
| 1214 |
+
dynamic_ncols=True,
|
| 1215 |
+
)
|
| 1216 |
+
|
| 1217 |
+
best_validation_loss, best_metrics = load_best_metrics(export_dir, resume_state)
|
| 1218 |
+
model.train()
|
| 1219 |
+
|
| 1220 |
+
chunk_ticks = int(rollout_cfg["chunk_ticks"])
|
| 1221 |
+
tick_seconds = float(rollout_cfg["tick_seconds"])
|
| 1222 |
+
|
| 1223 |
+
for epoch in range(start_epoch, int(training_cfg["num_train_epochs"])):
|
| 1224 |
+
epoch_train_loader = train_loader
|
| 1225 |
+
batch_index_offset = 0
|
| 1226 |
+
if epoch == start_epoch and batches_to_skip > 0:
|
| 1227 |
+
epoch_train_loader = accelerator.skip_first_batches(train_loader, batches_to_skip)
|
| 1228 |
+
batch_index_offset = batches_to_skip
|
| 1229 |
+
|
| 1230 |
+
for batch_index, conversations in enumerate(epoch_train_loader, start=batch_index_offset):
|
| 1231 |
+
batch_bucket = resolve_batch_duration_bucket(conversations)
|
| 1232 |
+
current_tbptt_steps = resolve_bucket_tbptt_steps(training_cfg, batch_bucket)
|
| 1233 |
+
current_horizon_ticks = resolve_bucket_horizon_ticks(batch_bucket, rollout_cfg)
|
| 1234 |
+
|
| 1235 |
+
z_state = model.initial_state(batch_size=len(conversations), device=accelerator.device)
|
| 1236 |
+
state_mask = model.initial_state_mask(batch_size=len(conversations), device=accelerator.device)
|
| 1237 |
+
max_chunks = max(int(conversation["chunk_count"]) for conversation in conversations)
|
| 1238 |
+
|
| 1239 |
+
for chunk_index in range(max_chunks):
|
| 1240 |
+
scheduled_decoder_fraction = resolve_decoder_trainable_fraction(training_cfg, global_step, total_update_steps)
|
| 1241 |
+
if scheduled_decoder_fraction is None:
|
| 1242 |
+
scheduled_decoder_fraction = 1.0
|
| 1243 |
+
if abs(scheduled_decoder_fraction - current_decoder_fraction) > 1e-6:
|
| 1244 |
+
current_decoder_fraction = scheduled_decoder_fraction
|
| 1245 |
+
current_decoder_trainable_layers = unwrapped_model.set_decoder_trainable_fraction(
|
| 1246 |
+
current_decoder_fraction
|
| 1247 |
+
)
|
| 1248 |
+
|
| 1249 |
+
scheduled_gate_head_frozen = should_freeze_gate_head(training_cfg, global_step, total_update_steps)
|
| 1250 |
+
if scheduled_gate_head_frozen != current_gate_head_frozen:
|
| 1251 |
+
current_gate_head_frozen = scheduled_gate_head_frozen
|
| 1252 |
+
unwrapped_model.set_gate_trainable(not current_gate_head_frozen)
|
| 1253 |
+
|
| 1254 |
+
chunk_batch = build_chunk_batch(
|
| 1255 |
+
conversations=conversations,
|
| 1256 |
+
chunk_index=chunk_index,
|
| 1257 |
+
pad_token_id=tokenizer.pad_token_id,
|
| 1258 |
+
chunk_ticks=chunk_ticks,
|
| 1259 |
+
tick_seconds=tick_seconds,
|
| 1260 |
+
)
|
| 1261 |
+
if chunk_batch is None:
|
| 1262 |
+
break
|
| 1263 |
+
chunk_batch = move_chunk_batch_to_device(chunk_batch, accelerator.device)
|
| 1264 |
+
|
| 1265 |
+
with accelerator.accumulate(model):
|
| 1266 |
+
total_loss, metrics, z_state, state_mask = compute_chunk_losses(
|
| 1267 |
+
model=model,
|
| 1268 |
+
batch=chunk_batch,
|
| 1269 |
+
training_cfg=training_cfg,
|
| 1270 |
+
z_state=z_state,
|
| 1271 |
+
state_mask=state_mask,
|
| 1272 |
+
tbptt_steps=current_tbptt_steps,
|
| 1273 |
+
freeze_gate_head_to_targets=current_gate_head_frozen,
|
| 1274 |
+
)
|
| 1275 |
+
accelerator.backward(total_loss)
|
| 1276 |
+
accelerator.clip_grad_norm_(model.parameters(), float(training_cfg["max_grad_norm"]))
|
| 1277 |
+
optimizer.step()
|
| 1278 |
+
scheduler.step()
|
| 1279 |
+
optimizer.zero_grad(set_to_none=True)
|
| 1280 |
+
|
| 1281 |
+
z_state = z_state.detach()
|
| 1282 |
+
state_mask = state_mask.detach()
|
| 1283 |
+
|
| 1284 |
+
if accelerator.sync_gradients:
|
| 1285 |
+
global_step += 1
|
| 1286 |
+
progress_bar.update(1)
|
| 1287 |
+
progress_bar.set_postfix(
|
| 1288 |
+
loss=f"{float(total_loss.detach()):.4f}",
|
| 1289 |
+
bucket=batch_bucket,
|
| 1290 |
+
tbptt=int(current_tbptt_steps or 0),
|
| 1291 |
+
horizon=int(current_horizon_ticks),
|
| 1292 |
+
decoder=current_decoder_trainable_layers,
|
| 1293 |
+
gate="locked" if current_gate_head_frozen else "train",
|
| 1294 |
+
refresh=False,
|
| 1295 |
+
)
|
| 1296 |
+
|
| 1297 |
+
if global_step % int(training_cfg["logging_steps"]) == 0:
|
| 1298 |
+
reduced = reduce_metrics(accelerator, [metrics], training_cfg)
|
| 1299 |
+
reduced["lr"] = scheduler.get_last_lr()[0]
|
| 1300 |
+
reduced["tbptt_steps"] = float(current_tbptt_steps or 0)
|
| 1301 |
+
reduced["horizon_ticks"] = float(current_horizon_ticks)
|
| 1302 |
+
reduced["chunk_ticks"] = float(chunk_ticks)
|
| 1303 |
+
reduced["decoder_trainable_layers"] = float(current_decoder_trainable_layers)
|
| 1304 |
+
reduced["gate_head_frozen"] = float(current_gate_head_frozen)
|
| 1305 |
+
reduced["epoch"] = epoch + 1
|
| 1306 |
+
reduced["global_step"] = global_step
|
| 1307 |
+
accelerator.log({f"train/{key}": value for key, value in reduced.items()}, step=global_step)
|
| 1308 |
+
|
| 1309 |
+
if global_step % int(training_cfg["eval_steps"]) == 0:
|
| 1310 |
+
validation_metrics = evaluate(
|
| 1311 |
+
accelerator=accelerator,
|
| 1312 |
+
model=model,
|
| 1313 |
+
dataloader=validation_loader,
|
| 1314 |
+
training_cfg=training_cfg,
|
| 1315 |
+
rollout_cfg=rollout_cfg,
|
| 1316 |
+
tokenizer_pad_token_id=tokenizer.pad_token_id,
|
| 1317 |
+
max_batches=training_cfg.get("eval_max_batches"),
|
| 1318 |
+
)
|
| 1319 |
+
accelerator.log(
|
| 1320 |
+
{f"validation/{key}": value for key, value in validation_metrics.items()},
|
| 1321 |
+
step=global_step,
|
| 1322 |
+
)
|
| 1323 |
+
|
| 1324 |
+
if validation_metrics and validation_metrics["loss_total"] < best_validation_loss:
|
| 1325 |
+
best_validation_loss = validation_metrics["loss_total"]
|
| 1326 |
+
best_metrics = validation_metrics
|
| 1327 |
+
export_model(
|
| 1328 |
+
accelerator=accelerator,
|
| 1329 |
+
model=model,
|
| 1330 |
+
output_dir=export_dir,
|
| 1331 |
+
config=config,
|
| 1332 |
+
config_path=config_path,
|
| 1333 |
+
best_metrics=best_metrics,
|
| 1334 |
+
)
|
| 1335 |
+
|
| 1336 |
+
if global_step % int(training_cfg["checkpoint_steps"]) == 0:
|
| 1337 |
+
checkpoint_dir = ensure_dir(run_dir / f"checkpoint-{global_step}")
|
| 1338 |
+
accelerator.save_state(str(checkpoint_dir))
|
| 1339 |
+
if accelerator.is_main_process:
|
| 1340 |
+
save_trainer_state(
|
| 1341 |
+
checkpoint_dir,
|
| 1342 |
+
global_step=global_step,
|
| 1343 |
+
epoch=epoch,
|
| 1344 |
+
next_batch_index=batch_index + 1,
|
| 1345 |
+
best_validation_loss=best_validation_loss,
|
| 1346 |
+
best_metrics=best_metrics,
|
| 1347 |
+
wandb_run_id=wandb_run_id,
|
| 1348 |
+
)
|
| 1349 |
+
accelerator.wait_for_everyone()
|
| 1350 |
+
maybe_push_checkpoint_to_hub(
|
| 1351 |
+
accelerator=accelerator,
|
| 1352 |
+
config=config,
|
| 1353 |
+
checkpoint_dir=checkpoint_dir,
|
| 1354 |
+
)
|
| 1355 |
+
accelerator.wait_for_everyone()
|
| 1356 |
+
|
| 1357 |
+
if not best_metrics:
|
| 1358 |
+
best_metrics = evaluate(
|
| 1359 |
+
accelerator=accelerator,
|
| 1360 |
+
model=model,
|
| 1361 |
+
dataloader=validation_loader,
|
| 1362 |
+
training_cfg=training_cfg,
|
| 1363 |
+
rollout_cfg=rollout_cfg,
|
| 1364 |
+
tokenizer_pad_token_id=tokenizer.pad_token_id,
|
| 1365 |
+
max_batches=training_cfg.get("eval_max_batches"),
|
| 1366 |
+
)
|
| 1367 |
+
export_model(
|
| 1368 |
+
accelerator=accelerator,
|
| 1369 |
+
model=model,
|
| 1370 |
+
output_dir=export_dir,
|
| 1371 |
+
config=config,
|
| 1372 |
+
config_path=config_path,
|
| 1373 |
+
best_metrics=best_metrics,
|
| 1374 |
+
)
|
| 1375 |
+
|
| 1376 |
+
if accelerator.is_main_process:
|
| 1377 |
+
training_summary = {
|
| 1378 |
+
"train_conversations": len(train_dataset),
|
| 1379 |
+
"validation_conversations": len(validation_dataset),
|
| 1380 |
+
"train_chunk_microsteps": total_chunk_microsteps,
|
| 1381 |
+
"trainable_parameters": accelerator.unwrap_model(model).trainable_parameter_count(),
|
| 1382 |
+
"best_metrics": best_metrics,
|
| 1383 |
+
"global_step": global_step,
|
| 1384 |
+
"resumed_from_checkpoint": str(resume_checkpoint) if resume_checkpoint is not None else None,
|
| 1385 |
+
"wandb_run_id": wandb_run_id,
|
| 1386 |
+
"decoder_layer_count": unwrapped_model.decoder_layer_count(),
|
| 1387 |
+
"tick_seconds": tick_seconds,
|
| 1388 |
+
"chunk_ticks": chunk_ticks,
|
| 1389 |
+
"tbptt_steps_by_bucket": training_cfg.get("tbptt_steps_by_bucket", {}),
|
| 1390 |
+
"horizon_ticks_by_bucket": rollout_cfg.get("horizon_ticks_by_bucket", {}),
|
| 1391 |
+
"shuffle_train": bool(training_cfg.get("shuffle_train", True)),
|
| 1392 |
+
"sort_dataset_by_length": sort_dataset_by_length,
|
| 1393 |
+
}
|
| 1394 |
+
save_json(run_dir / "training_summary.json", training_summary)
|
| 1395 |
+
maybe_push_to_hub(config, export_dir)
|
| 1396 |
+
|
| 1397 |
+
progress_bar.close()
|
| 1398 |
+
accelerator.end_training()
|
| 1399 |
+
|
| 1400 |
+
|
| 1401 |
+
if __name__ == "__main__":
|
| 1402 |
+
main()
|