Instructions to use AlexWortega/tinyvla with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- LeRobot
How to use AlexWortega/tinyvla with LeRobot:
- Notebooks
- Google Colab
- Kaggle
CRITICAL fix: torch.compile'd expert saved as expert._orig_mod.* -> from_pretrained silently dropped it (random flow head at eval). Save the uncompiled module
f0fcec5 verified | #!/usr/bin/env python | |
| """Training loop for shard-streamed data, tuned for a single H100. | |
| Differences from scripts/train.py (all of them are speed or correctness, none | |
| change the objective): | |
| data ShardSource (sequential parquet + JPEG) instead of LeRobotDataset | |
| random access into h264. This is the change that matters. | |
| attention expert routed through SDPA (see patches/modules_expert.py.diff) | |
| vision cam0 goes through the Qwen tower ONCE per step, not twice | |
| (patches/modeling_tinyvla.py.diff) | |
| tokens task strings pre-tokenized once into a lookup, not per step | |
| optimizer fused AdamW, foreach off, set_to_none | |
| precision bf16 autocast + TF32 matmuls, grad_accum 1 at a large batch | |
| compile torch.compile on the expert (many small ops; biggest compile win) | |
| resume shape-aware load: tensors whose shape changed (state/action | |
| projections when the action dim changes) are re-initialised and | |
| REPORTED, instead of raising or silently loading garbage | |
| Usage: | |
| # 1 GPU | |
| python train_fast.py --config configs/physical_ai_ft.yaml | |
| # N GPU (DDP): batch_size в конфиге — ПЕР-GPU; глобальный батч = batch_size*N | |
| torchrun --standalone --nproc_per_node 8 train_fast.py --config configs/physical_ai_ft.yaml | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import math | |
| import time | |
| from pathlib import Path | |
| import torch | |
| import yaml | |
| # --------------------------------------------------------------------- policy | |
| def make_policy(cfg: dict, state_dim: int, action_dim: int): | |
| from lerobot.configs import FeatureType, PolicyFeature | |
| from tinyvla.configuration_tinyvla import TinyVLAConfig | |
| from tinyvla.modeling_tinyvla import TinyVLAPolicy | |
| pcfg = TinyVLAConfig(**cfg.get("policy", {})) | |
| if action_dim > pcfg.max_action_dim or state_dim > pcfg.max_state_dim: | |
| raise SystemExit( | |
| f"dataset has state {state_dim}d / action {action_dim}d but the config caps them at " | |
| f"{pcfg.max_state_dim} / {pcfg.max_action_dim}. Raise max_state_dim/max_action_dim — " | |
| f"CanonicalSource._pad would TRUNCATE silently." | |
| ) | |
| s = pcfg.image_size | |
| pcfg.input_features = { | |
| "observation.images.cam0": PolicyFeature(type=FeatureType.VISUAL, shape=(3, s, s)), | |
| "observation.images.cam1": PolicyFeature(type=FeatureType.VISUAL, shape=(3, s, s)), | |
| "observation.state": PolicyFeature(type=FeatureType.STATE, shape=(pcfg.max_state_dim,)), | |
| } | |
| pcfg.output_features = {"action": PolicyFeature(type=FeatureType.ACTION, shape=(action_dim,))} | |
| pcfg.validate_features() | |
| return TinyVLAPolicy(pcfg), pcfg | |
| def load_compatible(model, path: Path): | |
| """Load a checkpoint, keeping only tensors whose shape still matches. | |
| Continuing C-scaled on a 58-DoF bimanual robot changes the action/state | |
| projections. torch's strict=False does NOT tolerate a shape change (it | |
| raises), so filter explicitly and say out loud what was dropped — a silently | |
| re-initialised action head is the difference between "fine-tuning" and | |
| "training a new head on a frozen trunk". | |
| """ | |
| from safetensors.torch import load_file | |
| sd = load_file(path / "model.safetensors") | |
| own = model.state_dict() | |
| keep, reshaped, expanded, unexpected = {}, [], [], [] | |
| for k, v in sd.items(): | |
| if k not in own: | |
| unexpected.append(k) | |
| elif own[k].shape != v.shape: | |
| # Rows grew, trailing dims intact (num_embodiments 16 -> 32): keep | |
| # the trained rows, new rows stay at init. A plain re-init here | |
| # would silently discard every trained embodiment embedding. | |
| if (own[k].ndim == v.ndim and own[k].shape[0] > v.shape[0] | |
| and own[k].shape[1:] == v.shape[1:]): | |
| merged = own[k].clone() | |
| merged[: v.shape[0]] = v | |
| keep[k] = merged | |
| expanded.append((k, tuple(v.shape), tuple(own[k].shape))) | |
| else: | |
| reshaped.append((k, tuple(v.shape), tuple(own[k].shape))) | |
| else: | |
| keep[k] = v | |
| missing = [k for k in own if k not in keep] | |
| model.load_state_dict(keep, strict=False) | |
| print(f"resume {path}: loaded {len(keep)}/{len(own)} tensors") | |
| for k, a, b in expanded: | |
| print(f" EXPANDED (rows grew, old rows kept) {k}: {a} -> {b}") | |
| for k, a, b in reshaped: | |
| print(f" RE-INIT (shape changed) {k}: {a} -> {b}") | |
| if unexpected: | |
| print(f" ignored {len(unexpected)} unexpected keys, e.g. {unexpected[:3]}") | |
| left = [k for k in missing if all(k != r[0] for r in reshaped)] | |
| if left: | |
| print(f" {len(left)} tensors kept at init, e.g. {left[:3]}") | |
| return len(keep) | |
| class Collate: | |
| """Picklable collate: tokenizes task strings with a per-process cache. | |
| A module-level class (not a closure) so DataLoader workers can be started | |
| with the 'spawn' context. spawn matters under DDP: forking workers from a | |
| process that already initialized CUDA/NCCL (with ffmpeg + rust-tokenizer | |
| threads alive) is exactly the fork-after-CUDA hazard that hung rank | |
| dataloaders in testing; spawned workers start clean. | |
| """ | |
| def __init__(self, model_name: str, max_length: int, morph_text_max_len: int = 32): | |
| self.model_name = model_name | |
| self.max_length = max_length | |
| self.morph_text_max_len = morph_text_max_len | |
| self._tokenizer = None | |
| self._cache: dict = {} | |
| self._mcache: dict = {} | |
| def _tok(self, tasks): | |
| if self._tokenizer is None: | |
| import os | |
| os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") | |
| from transformers import AutoTokenizer | |
| self._tokenizer = AutoTokenizer.from_pretrained(self.model_name) | |
| new = [t for t in set(tasks) if t not in self._cache] | |
| if new: | |
| e = self._tokenizer(new, padding="max_length", truncation=True, | |
| max_length=self.max_length, return_tensors="pt") | |
| for i, t in enumerate(new): | |
| self._cache[t] = (e["input_ids"][i], e["attention_mask"][i].bool()) | |
| return (torch.stack([self._cache[t][0] for t in tasks]), | |
| torch.stack([self._cache[t][1] for t in tasks])) | |
| def _mtok(self, texts): | |
| if self._tokenizer is None: | |
| self._tok([""]) # init tokenizer | |
| new = [t for t in set(texts) if t not in self._mcache] | |
| if new: | |
| e = self._tokenizer(new, padding="max_length", truncation=True, | |
| max_length=self.morph_text_max_len, return_tensors="pt") | |
| for i, t in enumerate(new): | |
| self._mcache[t] = (e["input_ids"][i], e["attention_mask"][i].bool()) | |
| return (torch.stack([self._mcache[t][0] for t in texts]), | |
| torch.stack([self._mcache[t][1] for t in texts])) | |
| def __getstate__(self): | |
| return {"model_name": self.model_name, "max_length": self.max_length, | |
| "morph_text_max_len": self.morph_text_max_len} | |
| def __setstate__(self, st): | |
| self.__init__(st["model_name"], st["max_length"], st.get("morph_text_max_len", 32)) | |
| def __call__(self, items): | |
| out = {} | |
| for k in items[0]: | |
| if k.startswith("__"): | |
| continue # webdataset re-injects __key__/__url__ after every stage | |
| if k == "task": | |
| ids, mask = self._tok([it["task"] for it in items]) | |
| out["observation.language.tokens"] = ids | |
| out["observation.language.attention_mask"] = mask | |
| elif k == "morph_text": | |
| ids, mask = self._mtok([it["morph_text"] for it in items]) | |
| out["morph_text_ids"] = ids | |
| out["morph_text_mask"] = mask | |
| else: | |
| out[k] = torch.stack([it[k] for it in items]) | |
| return out | |
| # ----------------------------------------------------------------------- main | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--config", type=Path, required=True) | |
| ap.add_argument("--profile-steps", type=int, default=0, | |
| help="run N steps, print throughput, exit (use to replace the estimate with a measurement)") | |
| args = ap.parse_args() | |
| cfg = yaml.safe_load(args.config.read_text()) | |
| torch.backends.cuda.matmul.allow_tf32 = True | |
| torch.backends.cudnn.allow_tf32 = True | |
| torch.backends.cudnn.benchmark = True | |
| # ---- DDP: активируется сам под torchrun, иначе одиночный GPU ---- | |
| import os as _osenv | |
| world = int(_osenv.environ.get("WORLD_SIZE", "1")) | |
| rank = int(_osenv.environ.get("RANK", "0")) | |
| local_rank = int(_osenv.environ.get("LOCAL_RANK", "0")) | |
| ddp = world > 1 | |
| if ddp: | |
| torch.distributed.init_process_group("nccl") | |
| torch.cuda.set_device(local_rank) | |
| is_main = rank == 0 | |
| def log(*a, **k): | |
| if is_main: | |
| print(*a, **k) | |
| dev = torch.device("cuda", local_rank if ddp else 0) | |
| # V100 (sm70) has no bf16: fall back to fp16 + GradScaler so debug runs on | |
| # older cards work; H100/B200 keep bf16 with the scaler disabled (no-op). | |
| # including_emulation=False: plain is_bf16_supported() returns True on | |
| # Volta (software emulation) and silently makes training ~10x slower. | |
| _bf16 = torch.cuda.is_bf16_supported(including_emulation=False) | |
| amp_dtype = torch.bfloat16 if _bf16 else torch.float16 | |
| scaler = torch.amp.GradScaler("cuda", enabled=amp_dtype is torch.float16) | |
| src_cfg = cfg["source"] | |
| morph = None | |
| if (src_cfg.get("type") != "hub" and cfg.get("morphology_descriptors") | |
| and cfg["policy"].get("conditioning") == "morph"): | |
| from tinyvla.modules.embodiment import MORPH_FIELDS | |
| raw = yaml.safe_load(Path(cfg["morphology_descriptors"]).read_text()) | |
| sc = {"arm_dof": 0.1, "reach_m": 2, "gripper_width_m": 10, "num_cameras": 1 / 3, | |
| "control_hz": 1 / 30, "joint_lo_mean": 1 / 3.1416, "joint_hi_mean": 1 / 3.1416, | |
| "workspace_x": 2, "workspace_y": 2, "workspace_z": 2, "payload_kg": 0.2} | |
| d = raw[src_cfg["morph_key"]] | |
| morph = torch.tensor([d.get(f, 0) * sc.get(f, 1) for f in MORPH_FIELDS], dtype=torch.float32) | |
| chunk = cfg["policy"]["chunk_size"] | |
| if src_cfg.get("type") == "hub": | |
| # stream episodes straight from HF hub (LeRobot v2.x), no local data at all | |
| import os as _os | |
| from tinyvla.data.streaming_hub import HubEpisodeStream | |
| specs = yaml.safe_load(Path(src_cfg["specs"]).read_text())["datasets"] | |
| tok_path = _os.path.expanduser("~/.cache/huggingface/token") | |
| pol = cfg["policy"] | |
| n_support = pol.get("n_support", 0) if (pol.get("use_demo_conditioning") or pol.get("vlm_native")) else 0 | |
| morph_map, prompt_map = {}, {} | |
| if cfg.get("morphology_descriptors"): | |
| from tinyvla.modules.embodiment import MORPH_FIELDS | |
| raw = yaml.safe_load(Path(cfg["morphology_descriptors"]).read_text()) | |
| sc = {"arm_dof": 0.1, "reach_m": 2, "gripper_width_m": 10, "num_cameras": 1 / 3, | |
| "control_hz": 1 / 30, "joint_lo_mean": 1 / 3.1416, "joint_hi_mean": 1 / 3.1416, | |
| "workspace_x": 2, "workspace_y": 2, "workspace_z": 2, "payload_kg": 0.2} | |
| for krobot, d in raw.items(): | |
| morph_map[krobot] = torch.tensor([d.get(f, 0) * sc.get(f, 1) for f in MORPH_FIELDS], | |
| dtype=torch.float32) | |
| if cfg.get("robot_prompts"): | |
| prompt_map = yaml.safe_load(Path(cfg["robot_prompts"]).read_text()) | |
| source = HubEpisodeStream( | |
| specs, | |
| token=open(tok_path).read().strip() if _os.path.exists(tok_path) else None, | |
| chunk=chunk, | |
| image_size=pol.get("image_size", 256), | |
| max_state_dim=pol["max_state_dim"], | |
| max_action_dim=pol["max_action_dim"], | |
| shuffle_buffer=cfg.get("shuffle_buffer", 4096), | |
| seed=cfg.get("seed", 42), | |
| rank=rank, | |
| world_size=world, | |
| n_support=n_support, | |
| morph_descriptors=morph_map, | |
| robot_prompts=prompt_map, | |
| ) | |
| log(f"source: HUB STREAM, {len(specs)} datasets (infinite mixture), world={world}") | |
| elif src_cfg.get("type") == "wds": | |
| # local self-contained webdataset tars (pack_wds.py / *_to_wds.py output), | |
| # mixed per-sample by weight; instructions resolved from tasks.json | |
| from tinyvla.data.wds_mix import MixtureSource | |
| specs = yaml.safe_load(Path(src_cfg["specs"]).read_text())["datasets"] | |
| root = Path(src_cfg.get("root", ".")) | |
| for s in specs: | |
| if "dir" not in s: # hf_repo: fetched beforehand by scripts/fetch_wds.py | |
| s["dir"] = str(root / s["hf_repo"].split("/")[-1] / s.get("subdir", "")) | |
| source = MixtureSource( | |
| specs, | |
| batch_size=cfg["batch_size"], | |
| num_workers=cfg.get("num_workers", 12), | |
| image_size=cfg["policy"].get("image_size", 256), | |
| max_state_dim=cfg["policy"]["max_state_dim"], | |
| max_action_dim=cfg["policy"]["max_action_dim"], | |
| shuffle_buffer=cfg.get("shuffle_buffer", 4096), | |
| steps_per_epoch=cfg.get("steps_per_epoch", 1000), | |
| seed=cfg.get("seed", 42) + rank, # decorrelate ranks: resampled streams | |
| ) | |
| log(f"source: WDS MIX, {len(specs)} datasets (infinite mixture), world={world}") | |
| elif src_cfg.get("type") == "wds_pack": | |
| # мелкие self-contained датасеты формата pack_wds (файнтюн на утёнке и т.п.) | |
| import json as _json | |
| from tinyvla.data.wds_pack import WdsPackSource | |
| pol = cfg["policy"] | |
| morph = None | |
| if cfg.get("morphology_descriptors") and src_cfg.get("morph_key"): | |
| from tinyvla.modules.embodiment import MORPH_FIELDS | |
| raw = yaml.safe_load(Path(cfg["morphology_descriptors"]).read_text()) | |
| sc = {"arm_dof": 0.1, "reach_m": 2, "gripper_width_m": 10, "num_cameras": 1 / 3, | |
| "control_hz": 1 / 30, "joint_lo_mean": 1 / 3.1416, "joint_hi_mean": 1 / 3.1416, | |
| "workspace_x": 2, "workspace_y": 2, "workspace_z": 2, "payload_kg": 0.2} | |
| d = raw[src_cfg["morph_key"]] | |
| morph = torch.tensor([d.get(f, 0) * sc.get(f, 1) for f in MORPH_FIELDS], | |
| dtype=torch.float32) | |
| tn = {} | |
| if src_cfg.get("task_names"): | |
| tn = {int(k): v for k, v in _json.loads(Path(src_cfg["task_names"]).read_text()).items()} | |
| source = WdsPackSource( | |
| root=src_cfg["root"], split=src_cfg.get("split", "train"), | |
| image_size=pol.get("image_size", 256), | |
| max_state_dim=pol["max_state_dim"], max_action_dim=pol["max_action_dim"], | |
| embodiment_id=src_cfg.get("embodiment_id", 0), | |
| n_support=pol.get("n_support", 0) if (pol.get("use_demo_conditioning") or pol.get("vlm_native")) else 0, | |
| task_names=tn, task_group_size=src_cfg.get("task_group_size", 0), | |
| morphology=morph, morph_text=src_cfg.get("robot_text", ""), | |
| seed=cfg.get("seed", 42), | |
| ) | |
| print(f"source: WDS_PACK {src_cfg['root']} [{src_cfg.get('split','train')}]: {len(source)} samples") | |
| else: | |
| from tinyvla.data.shards import ShardSource | |
| source = ShardSource( | |
| root=src_cfg["root"], | |
| embodiment_id=src_cfg.get("embodiment_id", 0), | |
| chunk=chunk, | |
| image_size=cfg["policy"].get("image_size", 256), | |
| max_state_dim=cfg["policy"]["max_state_dim"], | |
| max_action_dim=cfg["policy"]["max_action_dim"], | |
| morphology=morph, | |
| robot_prompt=src_cfg.get("robot_prompt"), | |
| shuffle_buffer=cfg.get("shuffle_buffer", 8192), | |
| seed=cfg.get("seed", 42), | |
| rank=rank, | |
| world_size=world, | |
| ) | |
| log(f"source: {source.num_frames:,} frames @ {source.fps} Hz, " | |
| f"state {source.state_dim}d action {source.action_dim}d, {len(source.shards)} shards") | |
| if src_cfg.get("type") in ("hub", "wds", "wds_pack"): | |
| # per-dataset dims vary; the source pads everything to the config caps | |
| policy, pcfg = make_policy(cfg, cfg["policy"]["max_state_dim"], cfg["policy"]["max_action_dim"]) | |
| else: | |
| policy, pcfg = make_policy(cfg, source.state_dim, source.action_dim) | |
| policy = policy.to(dev) | |
| if cfg.get("resume_from"): | |
| if is_main: | |
| load_compatible(policy, Path(cfg["resume_from"])) | |
| else: | |
| import contextlib, io as _io | |
| with contextlib.redirect_stdout(_io.StringIO()): | |
| load_compatible(policy, Path(cfg["resume_from"])) | |
| raw_policy = policy | |
| if ddp: | |
| policy = torch.nn.parallel.DistributedDataParallel( | |
| policy, device_ids=[local_rank], gradient_as_bucket_view=True | |
| ) | |
| # ---- pre-tokenize every distinct instruction once ----------------------- | |
| collate = Collate(pcfg.lm_model_name, pcfg.tokenizer_max_length, | |
| getattr(pcfg, "morph_text_max_len", 32)) | |
| nw = cfg.get("num_workers", 12) | |
| if ddp: | |
| # 16 воркеров/ранг x N рангов душат CPU и heartbeat torchrun-агента | |
| nw = cfg.get("num_workers_per_rank", max(4, nw // world)) | |
| log(f"DDP: {nw} dataloader workers per rank") | |
| hub = src_cfg.get("type") == "hub" | |
| map_style = not isinstance(source, torch.utils.data.IterableDataset) | |
| loader = torch.utils.data.DataLoader( | |
| source, | |
| batch_size=cfg["batch_size"], | |
| shuffle=map_style, | |
| num_workers=nw, | |
| pin_memory=True, | |
| persistent_workers=(nw > 0) and not hub, # hub: воркеры умирают каждую "эпоху" — сброс утечек | |
| prefetch_factor=cfg.get("prefetch_factor", 6) if nw > 0 else None, | |
| drop_last=True, | |
| collate_fn=collate, | |
| multiprocessing_context="spawn" if nw > 0 else None, | |
| ) | |
| # ---- optimizer: backbone at a lower lr, exactly as train.py does -------- | |
| backbone = [p for n, p in policy.named_parameters() if p.requires_grad and "semantic.vlm" in n] | |
| head = [p for n, p in policy.named_parameters() if p.requires_grad and "semantic.vlm" not in n] | |
| groups = [{"params": head, "lr": cfg["lr"]}] | |
| if backbone: | |
| groups.append({"params": backbone, "lr": cfg["lr"] * cfg.get("backbone_lr_mult", 0.1)}) | |
| opt = torch.optim.AdamW(groups, betas=(0.9, 0.95), weight_decay=1e-10, fused=True) | |
| log(f"trainable: head {sum(p.numel() for p in head)/1e6:.1f}M, " | |
| f"backbone {sum(p.numel() for p in backbone)/1e6:.1f}M at {cfg.get('backbone_lr_mult',0.1)}x lr") | |
| steps, warmup = cfg["steps"], cfg.get("warmup_steps", 1000) | |
| def lr_lambda(s): | |
| if s < warmup: | |
| return s / max(1, warmup) | |
| p = (s - warmup) / max(1, steps - warmup) | |
| return 0.025 + 0.975 * 0.5 * (1 + math.cos(math.pi * p)) | |
| sched = torch.optim.lr_scheduler.LambdaLR(opt, lr_lambda) | |
| orig_expert = raw_policy.expert # компилированный wrapper пишет state_dict с | |
| if cfg.get("compile", True): # префиксом expert._orig_mod.* — сейвим оригинал | |
| raw_policy.expert = torch.compile(raw_policy.expert, dynamic=False) | |
| log("torch.compile: expert") | |
| def save_clean(path): | |
| compiled = raw_policy.expert | |
| raw_policy.expert = orig_expert # параметры общие — это только про имена ключей | |
| raw_policy.save_pretrained(path) | |
| raw_policy.expert = compiled | |
| out_dir = Path(cfg["output_dir"]) | |
| if is_main: | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| if hasattr(source, "manifest"): | |
| (out_dir / "source_manifest.json").write_text(json.dumps(source.manifest, indent=2)) | |
| if cfg.get("wandb") and is_main: | |
| import wandb | |
| wandb.init(project=cfg["wandb"], config=cfg) | |
| grad_accum = cfg.get("grad_accum", 1) | |
| eff_batch = cfg["batch_size"] * grad_accum * world # глобальный батч | |
| log_freq = cfg.get("log_freq", 50) | |
| target = args.profile_steps or steps | |
| step, seen, t0 = 0, 0, time.time() | |
| data_iter = iter(loader) | |
| while step < target: | |
| opt.zero_grad(set_to_none=True) | |
| for _ in range(grad_accum): | |
| try: | |
| batch = next(data_iter) | |
| except StopIteration: | |
| if hasattr(source, "set_epoch"): | |
| source.set_epoch(getattr(source, "epoch", 0) + 1) | |
| data_iter = iter(loader) | |
| batch = next(data_iter) | |
| batch = {k: (v.to(dev, non_blocking=True) if torch.is_tensor(v) else v) | |
| for k, v in batch.items()} | |
| with torch.autocast("cuda", dtype=amp_dtype): | |
| loss, info = policy(batch) | |
| scaler.scale(loss / grad_accum).backward() | |
| seen += cfg["batch_size"] | |
| scaler.unscale_(opt) | |
| torch.nn.utils.clip_grad_norm_(raw_policy.parameters(), cfg.get("grad_clip", 10.0)) | |
| scaler.step(opt) | |
| scaler.update() | |
| sched.step() | |
| step += 1 | |
| if step % log_freq == 0: | |
| torch.cuda.synchronize() | |
| el = time.time() - t0 | |
| sps = seen * world / el # глобально по всем рангам | |
| # float(): yaml 1.1 парсит "2250.0e12" без знака экспоненты как строку | |
| gflops = float(cfg.get("gflops_per_sample", 360.0)) | |
| mfu = sps * gflops * 1e9 / float(cfg.get("peak_flops", 989e12)) * 100 | |
| eta = (steps - step) * eff_batch / sps / 3600 | |
| log(f"step {step}/{steps} loss {info['loss']:.4f} | {sps:.0f} samples/s " | |
| f"| {step/el:.2f} it/s | MFU~{mfu:.0f}% | eta {eta:.2f} h " | |
| f"| mem {torch.cuda.max_memory_allocated()/1e9:.1f} GB", flush=True) | |
| if cfg.get("wandb") and is_main: | |
| wandb.log({"loss": info["loss"], "lr": sched.get_last_lr()[0], | |
| "samples_per_s": sps}, step=step) | |
| seen, t0 = 0, time.time() | |
| if args.profile_steps == 0 and step % cfg.get("save_freq", 10000) == 0: | |
| if is_main: | |
| save_clean(out_dir / f"step_{step}") | |
| if ddp: | |
| torch.distributed.barrier() | |
| if args.profile_steps: | |
| log(f"\nprofile done: умножь samples/s выше на {steps * eff_batch:,} " | |
| f"глобальных семплов, чтобы получить реальный wall clock.") | |
| elif is_main: | |
| save_clean(out_dir / "final") | |
| if ddp: | |
| torch.distributed.barrier() | |
| torch.distributed.destroy_process_group() | |
| if __name__ == "__main__": | |
| main() | |