from __future__ import annotations import argparse import json import sys from pathlib import Path import yaml from common import ROOT, localize_workspace_path DEFAULT_CONFIG = ROOT / "plugins" / "1c" / "training" / "configs" / "qwen3-coder-30b-a3b-lora.yaml" DEFAULT_REQUIRED_BASE_FILES = [ "config.json", "tokenizer.json", "tokenizer_config.json", ] def load_config(path: Path) -> dict: with path.open("r", encoding="utf-8") as handle: data = yaml.safe_load(handle) if not isinstance(data, dict): raise ValueError(f"{path} must contain a YAML mapping") return data def count_jsonl(path: Path) -> int: if not path.exists(): return 0 count = 0 with path.open("r", encoding="utf-8") as handle: for line in handle: if line.strip(): json.loads(line) count += 1 return count def check_python_deps() -> list[str]: missing = [] for module in ("torch", "transformers", "datasets", "peft", "accelerate"): try: __import__(module) except Exception as exc: missing.append(f"{module}: {type(exc).__name__}: {exc}") return missing def check_cuda() -> tuple[bool, str]: try: import torch except Exception as exc: return False, f"torch unavailable: {exc}" if not torch.cuda.is_available(): return False, "torch.cuda.is_available() is false" return True, torch.cuda.get_device_name(0) def expected_base_files(config: dict) -> list[str]: configured = config.get("required_base_files") if isinstance(configured, list): return [str(item) for item in configured if str(item).strip()] return list(DEFAULT_REQUIRED_BASE_FILES) def missing_weight_shards(base_model_path: Path) -> list[str]: index_path = base_model_path / "model.safetensors.index.json" if not index_path.exists(): return [] try: index = json.loads(index_path.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: return [f"invalid model.safetensors.index.json: {exc}"] weight_map = index.get("weight_map") if not isinstance(weight_map, dict): return ["model.safetensors.index.json has no weight_map"] shards = sorted({str(value) for value in weight_map.values() if str(value).strip()}) return [name for name in shards if not (base_model_path / name).exists()] def main() -> int: parser = argparse.ArgumentParser(description="Preflight checks for 1C LoRA training.") parser.add_argument("--config", type=Path, default=DEFAULT_CONFIG) args = parser.parse_args() errors: list[str] = [] warnings: list[str] = [] config = load_config(args.config) dataset_path = localize_workspace_path(config["dataset_path"]) dataset_count = count_jsonl(dataset_path) if dataset_count == 0: errors.append(f"training dataset is missing or empty: {dataset_path}") elif dataset_count < 50: warnings.append(f"dataset has only {dataset_count} record(s); this is not enough for useful fine-tuning") base_model_path = localize_workspace_path(config["base_model_path"]) missing_files = [name for name in expected_base_files(config) if not (base_model_path / name).exists()] if missing_files: errors.append(f"base model is incomplete at {base_model_path}: missing {', '.join(missing_files)}") missing_shards = missing_weight_shards(base_model_path) if missing_shards: errors.append(f"base model shard set is incomplete at {base_model_path}: missing {', '.join(missing_shards)}") output_dir = localize_workspace_path(config["output_dir"]) if not output_dir.parent.exists(): warnings.append(f"adapter parent directory does not exist yet: {output_dir.parent}") missing_deps = check_python_deps() if missing_deps: errors.append(f"missing Python training dependencies: {'; '.join(missing_deps)}") cuda_ok, cuda_message = check_cuda() if cuda_ok: print(f"CUDA: {cuda_message}") else: errors.append(f"GPU/CUDA unavailable: {cuda_message}") for warning in warnings: print(f"WARNING: {warning}") if errors: print("1C training preflight failed:", file=sys.stderr) for error in errors: print(f"- {error}", file=sys.stderr) return 1 print("1C training preflight passed.") return 0 if __name__ == "__main__": raise SystemExit(main())