from __future__ import annotations import re from dataclasses import dataclass from pathlib import Path from typing import Any @dataclass(frozen=True) class LlamaFactoryCommand: command: list[str] work_dir: str env: dict[str, str] def validate_config(config: dict[str, Any]) -> list[str]: errors: list[str] = [] if not config.get("base_model") and not config.get("model_name_or_path"): errors.append("base_model or model_name_or_path is required") if not config.get("dataset") and not config.get("dataset_dir"): errors.append("dataset or dataset_dir is required") learning_rate = float(config.get("learning_rate", 0.0002)) if learning_rate <= 0: errors.append("learning_rate must be greater than zero") epochs = int(config.get("n_epochs", config.get("num_train_epochs", 1))) if epochs <= 0: errors.append("n_epochs must be greater than zero") return errors def build_command(config: dict[str, Any], llama_factory_home: str = "/app/LLaMA-Factory") -> LlamaFactoryCommand: errors = validate_config(config) if errors: raise ValueError("; ".join(errors)) model_path = config.get("base_model") or config.get("model_name_or_path") dataset = config.get("dataset") or config.get("dataset_dir") output_dir = config.get("output_dir") or f"/data/yg-ft/outputs/{config.get('name', 'sample-job')}" command = [ "llamafactory-cli", "train", "--stage", str(config.get("stage", "sft")).lower(), "--do_train", "true", "--model_name_or_path", str(model_path), "--dataset", str(dataset), "--template", str(config.get("template", "qwen")), "--finetuning_type", str(config.get("train_method", config.get("finetuning_type", "lora"))), "--output_dir", str(output_dir), "--per_device_train_batch_size", str(config.get("batch_size", 2)), "--learning_rate", str(config.get("learning_rate", 0.0002)), "--num_train_epochs", str(config.get("n_epochs", 3)), "--save_steps", str(config.get("save_steps", 50)), ] quantization_bit = int(config.get("quantization_bit", 0) or 0) if quantization_bit in {4, 8}: command.extend(["--quantization_bit", str(quantization_bit)]) return LlamaFactoryCommand(command=command, work_dir=str(Path(llama_factory_home)), env={}) def parse_log_line(line: str) -> dict[str, float] | None: if "loss" not in line or "learning_rate" not in line: return None result: dict[str, float] = {} for key in ["loss", "grad_norm", "learning_rate", "epoch"]: match = re.search(rf"['\"]?{key}['\"]?\s*:\s*([-+]?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)", line) if match: result[key] = float(match.group(1)) return result or None