Buckets:
| from __future__ import annotations | |
| import argparse | |
| import hashlib | |
| import importlib.util | |
| import json | |
| import os | |
| import shutil | |
| import sys | |
| import tempfile | |
| import time | |
| from datetime import UTC, datetime | |
| from pathlib import Path | |
| from typing import Any | |
| REQUIRED_LIVE_PACKAGES = ["torch", "transformers", "peft", "trl", "datasets", "accelerate", "bitsandbytes"] | |
| def utc_now() -> str: | |
| return datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z") | |
| def load_jsonl(path: Path) -> list[dict[str, Any]]: | |
| if not path.exists(): | |
| raise FileNotFoundError(f"JSONL file does not exist: {path}") | |
| rows: list[dict[str, Any]] = [] | |
| with path.open("r", encoding="utf-8", newline="") as handle: | |
| for line_no, line in enumerate(handle, start=1): | |
| if not line.strip(): | |
| continue | |
| try: | |
| item = json.loads(line) | |
| except json.JSONDecodeError as exc: | |
| raise ValueError( | |
| f"invalid JSONL at {path}:{line_no}: {exc.msg} " | |
| f"(column {exc.colno}, char {exc.pos})" | |
| ) from exc | |
| if not isinstance(item, dict): | |
| raise ValueError(f"record at {path}:{line_no} must be a JSON object") | |
| messages = item.get("messages") | |
| if not isinstance(messages, list) or not messages: | |
| raise ValueError(f"record at {path}:{line_no} must contain non-empty messages") | |
| rows.append(item) | |
| if not rows: | |
| raise ValueError(f"JSONL file has no records: {path}") | |
| return rows | |
| def sha256_file(path: Path) -> str: | |
| digest = hashlib.sha256() | |
| with path.open("rb") as handle: | |
| for chunk in iter(lambda: handle.read(1024 * 1024), b""): | |
| digest.update(chunk) | |
| return digest.hexdigest() | |
| def load_json(path: Path) -> dict[str, Any]: | |
| return json.loads(path.read_text(encoding="utf-8-sig")) | |
| def format_messages(record: dict[str, Any], tokenizer: Any | None = None) -> str: | |
| messages = record["messages"] | |
| if tokenizer is not None and getattr(tokenizer, "chat_template", None): | |
| return tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=False) | |
| parts: list[str] = [] | |
| for message in messages: | |
| if not isinstance(message, dict): | |
| raise ValueError("message must be an object") | |
| role = str(message.get("role", "")).strip() | |
| content = str(message.get("content", "")).strip() | |
| if not role or not content: | |
| raise ValueError("message role and content are required") | |
| parts.append(f"<|{role}|>\n{content}") | |
| return "\n".join(parts) + "\n<|end|>" | |
| def package_status() -> dict[str, bool]: | |
| return {name: importlib.util.find_spec(name) is not None for name in REQUIRED_LIVE_PACKAGES} | |
| def write_json(path: Path, payload: dict[str, Any]) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") | |
| class ZeroGradientStallError(RuntimeError): | |
| def __init__(self, *, step: int | None, zero_count: int, last_logs: dict[str, Any]) -> None: | |
| self.step = step | |
| self.zero_count = zero_count | |
| self.last_logs = last_logs | |
| super().__init__( | |
| "trainer reported zero gradient norm for " | |
| f"{zero_count} consecutive logged training events at step {step}" | |
| ) | |
| def trainable_parameter_summary(model: Any) -> dict[str, Any]: | |
| total_params = 0 | |
| trainable_params = 0 | |
| trainable_tensors = 0 | |
| trainable_prefix_counts: dict[str, int] = {} | |
| for name, param in model.named_parameters(): | |
| count = int(param.numel()) | |
| total_params += count | |
| if not getattr(param, "requires_grad", False): | |
| continue | |
| trainable_params += count | |
| trainable_tensors += 1 | |
| prefix = str(name).split(".")[0] | |
| trainable_prefix_counts[prefix] = trainable_prefix_counts.get(prefix, 0) + 1 | |
| pct = (trainable_params / total_params * 100.0) if total_params else 0.0 | |
| return { | |
| "schema_version": "shft_trainable_parameter_summary_v1", | |
| "total_params": total_params, | |
| "trainable_params": trainable_params, | |
| "trainable_tensors": trainable_tensors, | |
| "trainable_pct": round(pct, 6), | |
| "trainable_prefix_counts": trainable_prefix_counts, | |
| "ok": trainable_params > 0 and trainable_tensors > 0, | |
| } | |
| def gradient_parameter_summary(model: Any) -> dict[str, Any]: | |
| trainable_tensors = 0 | |
| grad_tensors = 0 | |
| nonzero_grad_tensors = 0 | |
| lora_trainable_tensors = 0 | |
| lora_nonzero_grad_tensors = 0 | |
| total_grad_norm = 0.0 | |
| sample_zero_grad_trainable: list[str] = [] | |
| sample_nonzero_grad_trainable: list[str] = [] | |
| for name, param in model.named_parameters(): | |
| if not getattr(param, "requires_grad", False): | |
| continue | |
| trainable_tensors += 1 | |
| is_lora = "lora_" in str(name).lower() | |
| if is_lora: | |
| lora_trainable_tensors += 1 | |
| grad = getattr(param, "grad", None) | |
| if grad is None: | |
| if len(sample_zero_grad_trainable) < 10: | |
| sample_zero_grad_trainable.append(str(name)) | |
| continue | |
| grad_tensors += 1 | |
| try: | |
| norm = float(grad.detach().float().norm().item()) | |
| except Exception: | |
| norm = 0.0 | |
| total_grad_norm += norm | |
| if norm > 0.0: | |
| nonzero_grad_tensors += 1 | |
| if is_lora: | |
| lora_nonzero_grad_tensors += 1 | |
| if len(sample_nonzero_grad_trainable) < 10: | |
| sample_nonzero_grad_trainable.append(str(name)) | |
| elif len(sample_zero_grad_trainable) < 10: | |
| sample_zero_grad_trainable.append(str(name)) | |
| return { | |
| "schema_version": "shft_gradient_parameter_summary_v1", | |
| "trainable_tensors": trainable_tensors, | |
| "grad_tensors": grad_tensors, | |
| "nonzero_grad_tensors": nonzero_grad_tensors, | |
| "lora_trainable_tensors": lora_trainable_tensors, | |
| "lora_nonzero_grad_tensors": lora_nonzero_grad_tensors, | |
| "total_grad_norm": total_grad_norm, | |
| "sample_zero_grad_trainable": sample_zero_grad_trainable, | |
| "sample_nonzero_grad_trainable": sample_nonzero_grad_trainable, | |
| "ok": nonzero_grad_tensors > 0 and (lora_trainable_tensors == 0 or lora_nonzero_grad_tensors > 0), | |
| } | |
| def copy_file_with_retry(source: Path, destination: Path, *, attempts: int = 5, delay_seconds: float = 2.0) -> None: | |
| destination.parent.mkdir(parents=True, exist_ok=True) | |
| last_error: OSError | None = None | |
| for attempt in range(1, attempts + 1): | |
| try: | |
| shutil.copyfile(source, destination) | |
| return | |
| except OSError as exc: | |
| last_error = exc | |
| if attempt == attempts: | |
| break | |
| time.sleep(delay_seconds * attempt) | |
| if last_error is not None: | |
| raise last_error | |
| def append_jsonl(path: Path, payload: dict[str, Any]) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| with path.open("a", encoding="utf-8", newline="\n") as handle: | |
| handle.write(json.dumps(payload, sort_keys=True) + "\n") | |
| def _jsonable(value: Any) -> Any: | |
| if value is None or isinstance(value, str | int | float | bool): | |
| return value | |
| if isinstance(value, dict): | |
| return {str(key): _jsonable(item) for key, item in value.items()} | |
| if isinstance(value, list | tuple): | |
| return [_jsonable(item) for item in value] | |
| try: | |
| return float(value) | |
| except (TypeError, ValueError): | |
| return str(value) | |
| def _number(value: Any, default: float | None = None) -> float | None: | |
| try: | |
| return float(value) | |
| except (TypeError, ValueError): | |
| return default | |
| def _metric_rows(log_history: list[dict[str, Any]]) -> list[dict[str, Any]]: | |
| rows: list[dict[str, Any]] = [] | |
| for item in log_history: | |
| row = {str(key): _jsonable(value) for key, value in item.items()} | |
| if "eval_loss" in row: | |
| row["event_type"] = "eval" | |
| elif "loss" in row: | |
| row["event_type"] = "train_log" | |
| else: | |
| row["event_type"] = "trainer_event" | |
| rows.append(row) | |
| return rows | |
| def _best_eval_row(rows: list[dict[str, Any]]) -> dict[str, Any] | None: | |
| eval_rows = [row for row in rows if _number(row.get("eval_loss")) is not None] | |
| if not eval_rows: | |
| return None | |
| return min(eval_rows, key=lambda row: _number(row.get("eval_loss"), float("inf")) or float("inf")) | |
| def _last_row_with(rows: list[dict[str, Any]], key: str) -> dict[str, Any] | None: | |
| for row in reversed(rows): | |
| if key in row and _number(row.get(key)) is not None: | |
| return row | |
| return None | |
| def build_trainer_metrics_summary( | |
| *, | |
| rows: list[dict[str, Any]], | |
| selected_checkpoint: dict[str, Any], | |
| overfit_tolerance: float, | |
| ) -> dict[str, Any]: | |
| best_eval = _best_eval_row(rows) | |
| final_eval = _last_row_with(rows, "eval_loss") | |
| final_train = _last_row_with(rows, "loss") | |
| best_eval_loss = _number(best_eval.get("eval_loss")) if best_eval else None | |
| final_eval_loss = _number(final_eval.get("eval_loss")) if final_eval else None | |
| final_train_loss = _number(final_train.get("loss")) if final_train else None | |
| selected_metric = _number(selected_checkpoint.get("selection_metric_value")) | |
| train_eval_gap = None | |
| if final_train_loss is not None and final_eval_loss is not None: | |
| train_eval_gap = round(final_eval_loss - final_train_loss, 6) | |
| late_eval_regression = None | |
| if best_eval_loss is not None and final_eval_loss is not None: | |
| late_eval_regression = round(final_eval_loss - best_eval_loss, 6) | |
| token_accuracy_rows = [ | |
| row | |
| for row in rows | |
| if _number(row.get("mean_token_accuracy")) is not None or _number(row.get("eval_mean_token_accuracy")) is not None | |
| ] | |
| first_token_accuracy = None | |
| final_token_accuracy = None | |
| if token_accuracy_rows: | |
| first_token_accuracy = _number( | |
| token_accuracy_rows[0].get("mean_token_accuracy") | |
| or token_accuracy_rows[0].get("eval_mean_token_accuracy") | |
| ) | |
| final_token_accuracy = _number( | |
| token_accuracy_rows[-1].get("mean_token_accuracy") | |
| or token_accuracy_rows[-1].get("eval_mean_token_accuracy") | |
| ) | |
| overfit_flags: list[str] = [] | |
| if late_eval_regression is not None and late_eval_regression > overfit_tolerance: | |
| overfit_flags.append("late_eval_loss_regression") | |
| return { | |
| "schema_version": "shft_trainer_metrics_summary_v1", | |
| "created_at": utc_now(), | |
| "metric_row_count": len(rows), | |
| "eval_row_count": len([row for row in rows if _number(row.get("eval_loss")) is not None]), | |
| "train_log_row_count": len([row for row in rows if _number(row.get("loss")) is not None]), | |
| "best_eval_step": best_eval.get("step") if best_eval else None, | |
| "best_eval_loss": best_eval_loss, | |
| "final_eval_step": final_eval.get("step") if final_eval else None, | |
| "final_eval_loss": final_eval_loss, | |
| "final_train_step": final_train.get("step") if final_train else None, | |
| "final_train_loss": final_train_loss, | |
| "train_eval_loss_gap": train_eval_gap, | |
| "late_eval_loss_regression": late_eval_regression, | |
| "first_token_accuracy": first_token_accuracy, | |
| "final_token_accuracy": final_token_accuracy, | |
| "selected_checkpoint": selected_checkpoint, | |
| "selected_metric_value": selected_metric, | |
| "overfit_tolerance": overfit_tolerance, | |
| "overfit_detected": bool(overfit_flags), | |
| "overfit_flags": overfit_flags, | |
| } | |
| def validate_dataset_provenance( | |
| args: argparse.Namespace, | |
| train_rows: list[dict[str, Any]], | |
| valid_rows: list[dict[str, Any]], | |
| ) -> dict[str, Any]: | |
| errors: list[str] = [] | |
| checks: dict[str, Any] = {} | |
| dataset_arg = getattr(args, "dataset_dir", None) | |
| dataset_dir = Path(dataset_arg) if dataset_arg else None | |
| manifest: dict[str, Any] | None = None | |
| train_sha = sha256_file(args.train_jsonl) | |
| valid_sha = sha256_file(args.valid_jsonl) | |
| if dataset_dir is not None: | |
| manifest_path = dataset_dir / "dataset_manifest.json" | |
| if not manifest_path.exists(): | |
| errors.append(f"missing dataset manifest: {manifest_path}") | |
| else: | |
| manifest = load_json(manifest_path) | |
| split_counts = manifest.get("split_counts", {}) | |
| split_hashes = manifest.get("split_sha256", {}) | |
| expected_train_count = int(split_counts.get("train", -1)) | |
| expected_valid_count = int(split_counts.get("valid", -1)) | |
| checks["manifest_train_records"] = { | |
| "ok": expected_train_count == len(train_rows), | |
| "detail": f"{len(train_rows)} == {expected_train_count}", | |
| } | |
| checks["manifest_valid_records"] = { | |
| "ok": expected_valid_count == len(valid_rows), | |
| "detail": f"{len(valid_rows)} == {expected_valid_count}", | |
| } | |
| if expected_train_count != len(train_rows): | |
| errors.append(f"manifest_train_records: {len(train_rows)} != {expected_train_count}") | |
| if expected_valid_count != len(valid_rows): | |
| errors.append(f"manifest_valid_records: {len(valid_rows)} != {expected_valid_count}") | |
| manifest_train_sha = split_hashes.get("train") | |
| manifest_valid_sha = split_hashes.get("valid") | |
| if manifest_train_sha: | |
| checks["manifest_train_sha256"] = { | |
| "ok": train_sha == manifest_train_sha, | |
| "detail": f"{train_sha} == {manifest_train_sha}", | |
| } | |
| if train_sha != manifest_train_sha: | |
| errors.append("manifest_train_sha256 mismatch") | |
| if manifest_valid_sha: | |
| checks["manifest_valid_sha256"] = { | |
| "ok": valid_sha == manifest_valid_sha, | |
| "detail": f"{valid_sha} == {manifest_valid_sha}", | |
| } | |
| if valid_sha != manifest_valid_sha: | |
| errors.append("manifest_valid_sha256 mismatch") | |
| expected_manifest_sha = (getattr(args, "expected_dataset_manifest_sha256", "") or "").strip() | |
| if expected_manifest_sha: | |
| actual_manifest_sha = sha256_file(manifest_path) | |
| checks["expected_dataset_manifest_sha256"] = { | |
| "ok": actual_manifest_sha == expected_manifest_sha, | |
| "detail": f"{actual_manifest_sha} == {expected_manifest_sha}", | |
| } | |
| if actual_manifest_sha != expected_manifest_sha: | |
| errors.append("expected_dataset_manifest_sha256 mismatch") | |
| expected_hashes = { | |
| "train": (getattr(args, "expected_train_sha256", "") or "").strip(), | |
| "valid": (getattr(args, "expected_valid_sha256", "") or "").strip(), | |
| } | |
| actual_hashes = {"train": train_sha, "valid": valid_sha} | |
| for split, expected in expected_hashes.items(): | |
| if not expected: | |
| continue | |
| ok = actual_hashes[split] == expected | |
| checks[f"expected_{split}_sha256"] = {"ok": ok, "detail": f"{actual_hashes[split]} == {expected}"} | |
| if not ok: | |
| errors.append(f"expected_{split}_sha256 mismatch") | |
| test_hash = "" | |
| expected_test_sha = (getattr(args, "expected_test_sha256", "") or "").strip() | |
| if dataset_dir is not None and (dataset_dir / "test.jsonl").exists(): | |
| test_hash = sha256_file(dataset_dir / "test.jsonl") | |
| if expected_test_sha: | |
| ok = test_hash == expected_test_sha | |
| checks["expected_test_sha256"] = {"ok": ok, "detail": f"{test_hash} == {expected_test_sha}"} | |
| if not ok: | |
| errors.append("expected_test_sha256 mismatch") | |
| return { | |
| "ok": not errors, | |
| "errors": errors, | |
| "checks": checks, | |
| "dataset_dir": str(dataset_dir) if dataset_dir else None, | |
| "train_jsonl": str(args.train_jsonl), | |
| "valid_jsonl": str(args.valid_jsonl), | |
| "actual_split_counts": {"train": len(train_rows), "valid": len(valid_rows)}, | |
| "actual_split_sha256": {"train": train_sha, "valid": valid_sha, "test": test_hash or None}, | |
| "manifest_split_counts": (manifest or {}).get("split_counts") if manifest else None, | |
| "manifest_split_sha256": (manifest or {}).get("split_sha256") if manifest else None, | |
| } | |
| def build_plan(args: argparse.Namespace, train_rows: list[dict[str, Any]], valid_rows: list[dict[str, Any]]) -> dict[str, Any]: | |
| missing = [name for name, present in package_status().items() if not present] | |
| min_steps = int(os.environ.get("SHFT_MIN_PRODUCTION_STEPS", "100")) | |
| min_train_records = int(os.environ.get("SHFT_MIN_PRODUCTION_TRAIN_RECORDS", "100")) | |
| readiness_warnings: list[str] = [] | |
| if args.max_steps < min_steps: | |
| readiness_warnings.append(f"max_steps={args.max_steps} is below production minimum {min_steps}; this is a smoke run") | |
| if len(train_rows) < min_train_records: | |
| readiness_warnings.append( | |
| f"train_records={len(train_rows)} is below production minimum {min_train_records}; corpus is too small for a durable role adapter" | |
| ) | |
| return { | |
| "run_id": args.run_id, | |
| "model_candidate": args.model_candidate, | |
| "finetune_start_policy": getattr(args, "finetune_start_policy", "bootstrap"), | |
| "start_adapter": getattr(args, "start_adapter", args.model_candidate), | |
| "base_model_id": args.base_model_id, | |
| "train_jsonl": str(args.train_jsonl), | |
| "valid_jsonl": str(args.valid_jsonl), | |
| "output_dir": str(args.output_dir), | |
| "train_records": len(train_rows), | |
| "valid_records": len(valid_rows), | |
| "dataset_provenance": validate_dataset_provenance(args, train_rows, valid_rows), | |
| "hyperparameters": { | |
| "max_steps": args.max_steps, | |
| "per_device_train_batch_size": args.per_device_train_batch_size, | |
| "gradient_accumulation_steps": args.gradient_accumulation_steps, | |
| "learning_rate": args.learning_rate, | |
| "lora_r": args.lora_r, | |
| "lora_alpha": args.lora_alpha, | |
| "lora_dropout": args.lora_dropout, | |
| "max_seq_length": args.max_seq_length, | |
| "logging_steps": getattr(args, "logging_steps", 5), | |
| "checkpoint_steps": getattr(args, "checkpoint_steps", 50), | |
| "eval_steps": getattr(args, "eval_steps", getattr(args, "checkpoint_steps", 50)), | |
| "save_total_limit": getattr(args, "save_total_limit", 4), | |
| "metric_for_best_model": getattr(args, "metric_for_best_model", "eval_loss"), | |
| "greater_is_better": getattr(args, "greater_is_better", False), | |
| "overfit_tolerance": getattr(args, "overfit_tolerance", 0.10), | |
| }, | |
| "quantization": { | |
| "load_in_4bit": True, | |
| "bnb_4bit_quant_type": "nf4", | |
| "bnb_4bit_compute_dtype": "bfloat16", | |
| }, | |
| "required_packages": package_status(), | |
| "missing_packages": missing, | |
| "readiness": { | |
| "production_candidate": not readiness_warnings, | |
| "warnings": readiness_warnings, | |
| "minimums": {"max_steps": min_steps, "train_records": min_train_records}, | |
| }, | |
| "dry_run": args.dry_run, | |
| "created_at": utc_now(), | |
| } | |
| def run_live_training(args: argparse.Namespace, plan: dict[str, Any]) -> dict[str, Any]: | |
| missing = plan["missing_packages"] | |
| if missing: | |
| return { | |
| "status": "blocked_missing_dependencies", | |
| "missing_packages": missing, | |
| "note": "Install the live ML stack or use an HF Jobs image that includes these packages.", | |
| } | |
| import torch | |
| from datasets import Dataset | |
| from peft import LoraConfig, PeftModel, get_peft_model, prepare_model_for_kbit_training | |
| from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, TrainerCallback | |
| from trl import SFTConfig, SFTTrainer | |
| class JsonlMetricsCallback(TrainerCallback): | |
| def __init__(self, path: Path) -> None: | |
| self.path = path | |
| self.started = time.time() | |
| def _write(self, event_type: str, state: Any, logs: dict[str, Any] | None = None) -> None: | |
| row = { | |
| "schema_version": "shft_trainer_metrics_v1", | |
| "event_type": event_type, | |
| "timestamp": utc_now(), | |
| "elapsed_seconds": round(time.time() - self.started, 3), | |
| "step": getattr(state, "global_step", None), | |
| "epoch": getattr(state, "epoch", None), | |
| } | |
| if logs: | |
| row.update({str(key): _jsonable(value) for key, value in logs.items()}) | |
| append_jsonl(self.path, row) | |
| def on_log(self, args: Any, state: Any, control: Any, logs: dict[str, Any] | None = None, **kwargs: Any) -> None: | |
| self._write("trainer_log", state, logs) | |
| def on_evaluate(self, args: Any, state: Any, control: Any, metrics: dict[str, Any] | None = None, **kwargs: Any) -> None: | |
| self._write("eval", state, metrics) | |
| def on_save(self, args: Any, state: Any, control: Any, **kwargs: Any) -> None: | |
| self._write("save", state, None) | |
| def on_train_end(self, args: Any, state: Any, control: Any, **kwargs: Any) -> None: | |
| self._write("train_end", state, None) | |
| class ZeroGradientStallCallback(TrainerCallback): | |
| def __init__(self, path: Path, *, max_zero_logs: int = 3, hard_halt_enabled: bool = True) -> None: | |
| self.path = path | |
| self.max_zero_logs = max(1, max_zero_logs) | |
| self.hard_halt_enabled = hard_halt_enabled | |
| self.zero_count = 0 | |
| def on_log(self, args: Any, state: Any, control: Any, logs: dict[str, Any] | None = None, **kwargs: Any) -> None: | |
| if not logs or "grad_norm" not in logs: | |
| return | |
| try: | |
| grad_norm = float(logs["grad_norm"]) | |
| except (TypeError, ValueError): | |
| return | |
| if grad_norm > 0.0: | |
| self.zero_count = 0 | |
| return | |
| self.zero_count += 1 | |
| step = getattr(state, "global_step", None) | |
| row = { | |
| "schema_version": "shft_zero_gradient_stall_v1", | |
| "event_type": "zero_gradient_observed", | |
| "timestamp": utc_now(), | |
| "step": step, | |
| "zero_count": self.zero_count, | |
| "max_zero_logs": self.max_zero_logs, | |
| "hard_halt_enabled": self.hard_halt_enabled, | |
| "grad_norm": grad_norm, | |
| "logs": {str(key): _jsonable(value) for key, value in logs.items()}, | |
| } | |
| append_jsonl(self.path, row) | |
| if self.hard_halt_enabled and self.zero_count >= self.max_zero_logs: | |
| halt = ( | |
| "[SHFT TRAIN STALL] ACTION=HALT_ZERO_GRADIENT_TRAINING " | |
| f"step={step} consecutive_zero_grad_logs={self.zero_count} " | |
| "reason=trainer_reported_zero_grad_norm_repeatedly" | |
| ) | |
| print(halt) | |
| print(halt, file=sys.stderr) | |
| raise ZeroGradientStallError(step=step, zero_count=self.zero_count, last_logs=logs) | |
| train_rows = load_jsonl(args.train_jsonl) | |
| valid_rows = load_jsonl(args.valid_jsonl) | |
| tokenizer = AutoTokenizer.from_pretrained(args.base_model_id, use_fast=True) | |
| if tokenizer.pad_token is None: | |
| tokenizer.pad_token = tokenizer.eos_token | |
| train_dataset = Dataset.from_list([{"text": format_messages(row, tokenizer)} for row in train_rows]) | |
| valid_dataset = Dataset.from_list([{"text": format_messages(row, tokenizer)} for row in valid_rows]) | |
| quantization = BitsAndBytesConfig( | |
| load_in_4bit=True, | |
| bnb_4bit_quant_type="nf4", | |
| bnb_4bit_compute_dtype=torch.bfloat16, | |
| ) | |
| model = AutoModelForCausalLM.from_pretrained( | |
| args.base_model_id, | |
| quantization_config=quantization, | |
| device_map="auto", | |
| trust_remote_code=False, | |
| ) | |
| if getattr(model, "config", None) is not None: | |
| model.config.use_cache = False | |
| model = prepare_model_for_kbit_training(model) | |
| if hasattr(model, "enable_input_require_grads"): | |
| model.enable_input_require_grads() | |
| start_adapter = args.start_adapter.strip() if args.start_adapter else "" | |
| if start_adapter: | |
| model = PeftModel.from_pretrained(model, start_adapter, is_trainable=True) | |
| else: | |
| lora_config = LoraConfig( | |
| r=args.lora_r, | |
| lora_alpha=args.lora_alpha, | |
| lora_dropout=args.lora_dropout, | |
| bias="none", | |
| task_type="CAUSAL_LM", | |
| target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], | |
| ) | |
| model = get_peft_model(model, lora_config) | |
| trainable_summary = trainable_parameter_summary(model) | |
| write_json(args.output_dir / "trainable_parameters.json", trainable_summary) | |
| print("[SHFT TRAINABLE PARAMETERS] " + json.dumps(trainable_summary, sort_keys=True)) | |
| if not trainable_summary["ok"]: | |
| print("[SHFT TRAIN STALL] ACTION=HALT_NO_TRAINABLE_PARAMETERS", file=sys.stderr) | |
| return { | |
| "status": "blocked_no_trainable_parameters", | |
| "run_id": args.run_id, | |
| "completed_at": utc_now(), | |
| "trainable_parameters": trainable_summary, | |
| "trainable_parameters_path": str(args.output_dir / "trainable_parameters.json"), | |
| } | |
| preflight_path = args.output_dir / "gradient_preflight.json" | |
| try: | |
| model.train() | |
| model.zero_grad(set_to_none=True) | |
| smoke_text = train_dataset[0]["text"] | |
| encoded = tokenizer( | |
| smoke_text, | |
| return_tensors="pt", | |
| truncation=True, | |
| max_length=args.max_seq_length, | |
| ) | |
| if int(encoded["input_ids"].numel()) < 2: | |
| raise ValueError("gradient preflight sample tokenized to fewer than 2 tokens") | |
| labels = encoded["input_ids"].clone() | |
| encoded["labels"] = labels | |
| first_param = next(param for param in model.parameters() if getattr(param, "requires_grad", False)) | |
| encoded = {key: value.to(first_param.device) for key, value in encoded.items()} | |
| with torch.enable_grad(): | |
| outputs = model(**encoded) | |
| loss = outputs.loss | |
| loss.backward() | |
| gradient_summary = gradient_parameter_summary(model) | |
| preflight = { | |
| "schema_version": "shft_gradient_preflight_v1", | |
| "run_id": args.run_id, | |
| "created_at": utc_now(), | |
| "base_model_id": args.base_model_id, | |
| "max_seq_length": args.max_seq_length, | |
| "sample_token_count": int(encoded["input_ids"].numel()), | |
| "loss": float(loss.detach().float().item()), | |
| "trainable_parameters": trainable_summary, | |
| "gradient_parameters": gradient_summary, | |
| "ok": bool(gradient_summary["ok"]), | |
| "quit_policy": "halt before paid full training unless a one-batch backward pass proves nonzero trainable LoRA gradients", | |
| } | |
| write_json(preflight_path, preflight) | |
| print("[SHFT GRADIENT PREFLIGHT] " + json.dumps(preflight, sort_keys=True)) | |
| if not preflight["ok"]: | |
| halt = ( | |
| "[SHFT TRAIN STALL] ACTION=HALT_GRADIENT_PREFLIGHT_FAILED " | |
| f"nonzero_grad_tensors={gradient_summary['nonzero_grad_tensors']} " | |
| f"lora_nonzero_grad_tensors={gradient_summary['lora_nonzero_grad_tensors']} " | |
| "reason=one_batch_backward_did_not_prove_trainable_lora_gradients" | |
| ) | |
| print(halt) | |
| print(halt, file=sys.stderr) | |
| return { | |
| "status": "blocked_gradient_preflight_failed", | |
| "run_id": args.run_id, | |
| "completed_at": utc_now(), | |
| "trainable_parameters": trainable_summary, | |
| "trainable_parameters_path": str(args.output_dir / "trainable_parameters.json"), | |
| "gradient_preflight": preflight, | |
| "gradient_preflight_path": str(preflight_path), | |
| "quit_policy": preflight["quit_policy"], | |
| } | |
| except Exception as exc: | |
| preflight = { | |
| "schema_version": "shft_gradient_preflight_v1", | |
| "run_id": args.run_id, | |
| "created_at": utc_now(), | |
| "base_model_id": args.base_model_id, | |
| "trainable_parameters": trainable_summary, | |
| "ok": False, | |
| "error": str(exc), | |
| "quit_policy": "halt before paid full training when gradient preflight cannot complete", | |
| } | |
| write_json(preflight_path, preflight) | |
| print("[SHFT TRAIN STALL] ACTION=HALT_GRADIENT_PREFLIGHT_ERROR reason=" + str(exc), file=sys.stderr) | |
| return { | |
| "status": "blocked_gradient_preflight_error", | |
| "run_id": args.run_id, | |
| "completed_at": utc_now(), | |
| "trainable_parameters": trainable_summary, | |
| "trainable_parameters_path": str(args.output_dir / "trainable_parameters.json"), | |
| "gradient_preflight": preflight, | |
| "gradient_preflight_path": str(preflight_path), | |
| "quit_policy": preflight["quit_policy"], | |
| } | |
| finally: | |
| model.zero_grad(set_to_none=True) | |
| checkpoint_steps = max(1, min(int(args.checkpoint_steps), int(args.max_steps))) | |
| eval_steps = max(1, min(int(args.eval_steps or args.checkpoint_steps), int(args.max_steps))) | |
| if checkpoint_steps % eval_steps != 0: | |
| checkpoint_steps = eval_steps | |
| logging_steps = max(1, int(args.logging_steps)) | |
| training_args = SFTConfig( | |
| output_dir=str(args.output_dir), | |
| max_steps=args.max_steps, | |
| per_device_train_batch_size=args.per_device_train_batch_size, | |
| gradient_accumulation_steps=args.gradient_accumulation_steps, | |
| learning_rate=args.learning_rate, | |
| logging_steps=logging_steps, | |
| save_strategy="steps", | |
| save_steps=checkpoint_steps, | |
| eval_strategy="steps", | |
| eval_steps=eval_steps, | |
| save_total_limit=args.save_total_limit, | |
| load_best_model_at_end=True, | |
| metric_for_best_model=args.metric_for_best_model, | |
| greater_is_better=args.greater_is_better, | |
| report_to=[], | |
| bf16=True, | |
| gradient_checkpointing=True, | |
| gradient_checkpointing_kwargs={"use_reentrant": False}, | |
| dataset_text_field="text", | |
| max_length=args.max_seq_length, | |
| ) | |
| metrics_path = args.output_dir / "trainer_metrics.jsonl" | |
| local_metrics_dir = Path(tempfile.gettempdir()) / "linvest21_shft_metrics" / args.run_id | |
| local_metrics_path = local_metrics_dir / "trainer_metrics.jsonl" | |
| if local_metrics_path.exists(): | |
| local_metrics_path.unlink() | |
| trainer = SFTTrainer( | |
| model=model, | |
| args=training_args, | |
| train_dataset=train_dataset, | |
| eval_dataset=valid_dataset, | |
| processing_class=tokenizer, | |
| callbacks=[ | |
| JsonlMetricsCallback(local_metrics_path), | |
| ZeroGradientStallCallback(local_metrics_path, hard_halt_enabled=True), | |
| ], | |
| ) | |
| try: | |
| result = trainer.train() | |
| except ZeroGradientStallError as exc: | |
| if local_metrics_path.exists(): | |
| copy_file_with_retry(local_metrics_path, metrics_path) | |
| return { | |
| "status": "blocked_zero_gradient_stall", | |
| "run_id": args.run_id, | |
| "completed_at": utc_now(), | |
| "stall_reason": str(exc), | |
| "stall_step": exc.step, | |
| "consecutive_zero_grad_logs": exc.zero_count, | |
| "last_logs": {str(key): _jsonable(value) for key, value in exc.last_logs.items()}, | |
| "trainable_parameters": trainable_summary, | |
| "trainable_parameters_path": str(args.output_dir / "trainable_parameters.json"), | |
| "trainer_metrics": str(metrics_path), | |
| "quit_policy": "halt paid training when trainable parameters exist but gradient norm is repeatedly zero", | |
| } | |
| metric_rows = _metric_rows(getattr(trainer.state, "log_history", [])) | |
| if not local_metrics_path.exists() and metric_rows: | |
| for row in metric_rows: | |
| append_jsonl(local_metrics_path, row) | |
| if local_metrics_path.exists(): | |
| copy_file_with_retry(local_metrics_path, metrics_path) | |
| best_checkpoint = getattr(trainer.state, "best_model_checkpoint", None) | |
| best_metric = getattr(trainer.state, "best_metric", None) | |
| best_eval = _best_eval_row(metric_rows) | |
| if best_metric is None and best_eval is not None: | |
| best_metric = best_eval.get(args.metric_for_best_model) | |
| if not best_checkpoint and best_eval is not None and best_eval.get("step") is not None: | |
| candidate = args.output_dir / f"checkpoint-{best_eval['step']}" | |
| if candidate.exists(): | |
| best_checkpoint = str(candidate) | |
| final_eval = _last_row_with(metric_rows, args.metric_for_best_model) | |
| selected_checkpoint = { | |
| "schema_version": "shft_selected_checkpoint_v1", | |
| "run_id": args.run_id, | |
| "created_at": utc_now(), | |
| "selection_metric": args.metric_for_best_model, | |
| "selection_metric_value": _number(best_metric), | |
| "selected_checkpoint": best_checkpoint, | |
| "selected_step": best_eval.get("step") if best_eval else None, | |
| "final_step": getattr(trainer.state, "global_step", None), | |
| "final_metric_value": _number(final_eval.get(args.metric_for_best_model)) if final_eval else None, | |
| "greater_is_better": args.greater_is_better, | |
| "candidate_adapter_dir": str(args.output_dir / "adapter"), | |
| "selection_policy": "trainer_best_model_checkpoint_by_validation_metric", | |
| } | |
| selected_metric = _number(selected_checkpoint.get("selection_metric_value")) | |
| final_metric = _number(selected_checkpoint.get("final_metric_value")) | |
| selected_checkpoint["selected_vs_final_delta"] = ( | |
| round(final_metric - selected_metric, 6) | |
| if selected_metric is not None and final_metric is not None | |
| else None | |
| ) | |
| selected_checkpoint["overfit_detected"] = ( | |
| selected_checkpoint["selected_vs_final_delta"] is not None | |
| and selected_checkpoint["selected_vs_final_delta"] > args.overfit_tolerance | |
| ) | |
| write_json(args.output_dir / "selected_checkpoint.json", selected_checkpoint) | |
| summary = build_trainer_metrics_summary( | |
| rows=metric_rows, | |
| selected_checkpoint=selected_checkpoint, | |
| overfit_tolerance=args.overfit_tolerance, | |
| ) | |
| write_json(args.output_dir / "trainer_metrics_summary.json", summary) | |
| trainer.save_model(str(args.output_dir / "adapter")) | |
| tokenizer.save_pretrained(str(args.output_dir / "adapter")) | |
| return { | |
| "status": "completed", | |
| "run_id": args.run_id, | |
| "completed_at": utc_now(), | |
| "train_loss": getattr(result, "training_loss", None), | |
| "adapter_dir": str(args.output_dir / "adapter"), | |
| "trainer_metrics": str(metrics_path), | |
| "trainer_metrics_summary": str(args.output_dir / "trainer_metrics_summary.json"), | |
| "selected_checkpoint": selected_checkpoint, | |
| "selected_checkpoint_path": str(args.output_dir / "selected_checkpoint.json"), | |
| "overfit_detected": summary["overfit_detected"], | |
| "overfit_flags": summary["overfit_flags"], | |
| } | |
| def main() -> int: | |
| parser = argparse.ArgumentParser(description="Linvest21 SHFT Hugging Face QLoRA/PEFT continuation trainer.") | |
| parser.add_argument("--run-id", required=True) | |
| parser.add_argument("--model-candidate", required=True) | |
| parser.add_argument("--start-adapter") | |
| parser.add_argument("--finetune-start-policy", default="bootstrap", choices=["bootstrap", "continue-best"]) | |
| parser.add_argument("--base-model-id", default="meta-llama/Meta-Llama-3-8B") | |
| parser.add_argument("--dataset-dir") | |
| parser.add_argument("--train-jsonl") | |
| parser.add_argument("--valid-jsonl") | |
| parser.add_argument("--output-dir", required=True) | |
| parser.add_argument("--max-steps", type=int, default=20) | |
| parser.add_argument("--per-device-train-batch-size", type=int, default=1) | |
| parser.add_argument("--gradient-accumulation-steps", type=int, default=8) | |
| parser.add_argument("--learning-rate", type=float, default=0.00008) | |
| parser.add_argument("--lora-r", type=int, default=16) | |
| parser.add_argument("--lora-alpha", type=int, default=32) | |
| parser.add_argument("--lora-dropout", type=float, default=0.05) | |
| parser.add_argument("--max-seq-length", type=int, default=2048) | |
| parser.add_argument("--logging-steps", type=int, default=5) | |
| parser.add_argument("--checkpoint-steps", type=int, default=int(os.environ.get("SHFT_CHECKPOINT_STEPS", "50"))) | |
| parser.add_argument("--eval-steps", type=int, default=int(os.environ.get("SHFT_EVAL_STEPS", "50"))) | |
| parser.add_argument("--save-total-limit", type=int, default=int(os.environ.get("SHFT_SAVE_TOTAL_LIMIT", "4"))) | |
| parser.add_argument("--metric-for-best-model", default=os.environ.get("SHFT_METRIC_FOR_BEST_MODEL", "eval_loss")) | |
| parser.add_argument("--greater-is-better", action="store_true") | |
| parser.add_argument("--overfit-tolerance", type=float, default=float(os.environ.get("SHFT_OVERFIT_TOLERANCE", "0.10"))) | |
| parser.add_argument("--dry-run", action="store_true") | |
| parser.add_argument("--expected-dataset-manifest-sha256", default="") | |
| parser.add_argument("--expected-train-sha256", default="") | |
| parser.add_argument("--expected-valid-sha256", default="") | |
| parser.add_argument("--expected-test-sha256", default="") | |
| args = parser.parse_args() | |
| dataset_dir = Path(args.dataset_dir) if args.dataset_dir else None | |
| args.train_jsonl = Path(args.train_jsonl) if args.train_jsonl else (dataset_dir / "train.jsonl" if dataset_dir else None) | |
| args.valid_jsonl = Path(args.valid_jsonl) if args.valid_jsonl else (dataset_dir / "valid.jsonl" if dataset_dir else None) | |
| args.output_dir = Path(args.output_dir) | |
| if args.train_jsonl is None or args.valid_jsonl is None: | |
| raise SystemExit("--dataset-dir or both --train-jsonl and --valid-jsonl are required") | |
| train_rows = load_jsonl(args.train_jsonl) | |
| valid_rows = load_jsonl(args.valid_jsonl) | |
| args.output_dir.mkdir(parents=True, exist_ok=True) | |
| plan = build_plan(args, train_rows, valid_rows) | |
| write_json(args.output_dir / "training_plan.json", plan) | |
| if not plan["dataset_provenance"]["ok"]: | |
| result = { | |
| "status": "blocked_dataset_provenance_mismatch", | |
| "run_id": args.run_id, | |
| "completed_at": utc_now(), | |
| "errors": plan["dataset_provenance"]["errors"], | |
| "plan_path": str(args.output_dir / "training_plan.json"), | |
| } | |
| write_json(args.output_dir / "training_result.json", result) | |
| print(json.dumps(result, indent=2)) | |
| return 2 | |
| if args.dry_run: | |
| result = { | |
| "status": "dry_run_validated", | |
| "run_id": args.run_id, | |
| "completed_at": utc_now(), | |
| "formatted_preview": format_messages(train_rows[0])[:500], | |
| "plan_path": str(args.output_dir / "training_plan.json"), | |
| } | |
| write_json(args.output_dir / "training_result.json", result) | |
| print(json.dumps(result, indent=2)) | |
| return 0 | |
| result = run_live_training(args, plan) | |
| write_json(args.output_dir / "training_result.json", result) | |
| print(json.dumps(result, indent=2)) | |
| return 0 if result["status"] == "completed" else 2 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |
Xet Storage Details
- Size:
- 40.6 kB
- Xet hash:
- 13718f364997782fee31f42aef93e7c77ca4fa5a7ace860dbca5d40472e26498
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.