| """ |
| lora_plan.py + distill.py — Adapter training plans and distillation runner. |
| |
| These are DRY-RUN planning modules. They produce configs and plans |
| but do NOT execute training (that requires purpose-agent[train] extra). |
| |
| Key rule: no distillation without eval data AND ROI check. |
| """ |
| from __future__ import annotations |
| from dataclasses import dataclass, field |
| from typing import Any, Literal |
|
|
|
|
| @dataclass |
| class DistillationPlan: |
| """ |
| Plan for creating a task-native mini-model or adapter. |
| |
| Modes: |
| none → no optimization needed (performance is fine) |
| prompt_pack → optimize prompts only (cheapest) |
| lora → LoRA/QLoRA adapter on base model |
| distill → full knowledge distillation teacher→student |
| pdq → prune → distill → quantize (maximum compression) |
| """ |
| mode: Literal["none", "prompt_pack", "lora", "distill", "pdq"] = "none" |
| teacher_model: str = "" |
| student_base: str = "" |
| dataset_path: str = "" |
| eval_path: str = "" |
| target_format: str = "gguf" |
| acceptance_score: float = 0.9 |
| rollback_model: str = "" |
| estimated_cost_usd: float = 0.0 |
| estimated_time_hours: float = 0.0 |
| reason: str = "" |
| metadata: dict[str, Any] = field(default_factory=dict) |
|
|
| @property |
| def requires_gpu(self) -> bool: |
| return self.mode in ("lora", "distill", "pdq") |
|
|
| @property |
| def requires_train_extra(self) -> bool: |
| return self.mode in ("lora", "distill", "pdq") |
|
|
| def to_dict(self) -> dict[str, Any]: |
| return { |
| "mode": self.mode, "teacher_model": self.teacher_model, |
| "student_base": self.student_base, "dataset_path": self.dataset_path, |
| "eval_path": self.eval_path, "target_format": self.target_format, |
| "acceptance_score": self.acceptance_score, "rollback_model": self.rollback_model, |
| "estimated_cost_usd": self.estimated_cost_usd, |
| "estimated_time_hours": self.estimated_time_hours, |
| "reason": self.reason, "requires_gpu": self.requires_gpu, |
| } |
|
|
| def summary(self) -> str: |
| if self.mode == "none": |
| return "No optimization needed." |
| return ( |
| f"Plan: {self.mode}\n" |
| f" Teacher: {self.teacher_model}\n" |
| f" Student: {self.student_base}\n" |
| f" Dataset: {self.dataset_path}\n" |
| f" Acceptance: {self.acceptance_score:.0%}\n" |
| f" Est. cost: ${self.estimated_cost_usd:.2f}\n" |
| f" Est. time: {self.estimated_time_hours:.1f}h\n" |
| f" Reason: {self.reason}" |
| ) |
|
|
|
|
| def plan_distillation( |
| fingerprint: dict[str, Any], |
| dataset_size: int, |
| current_model: str = "", |
| target_model: str = "", |
| has_gpu: bool = False, |
| ) -> DistillationPlan: |
| """ |
| Create a distillation plan based on capability fingerprint and available resources. |
| |
| Rules: |
| - No dataset → prompt_pack only |
| - Dataset < 100 examples → prompt_pack only |
| - Dataset 100-1000 + GPU → LoRA |
| - Dataset > 1000 + GPU → full distill |
| - No GPU → prompt_pack regardless |
| """ |
| if dataset_size < 10: |
| return DistillationPlan(mode="none", reason="Insufficient data for any optimization") |
|
|
| if dataset_size < 100 or not has_gpu: |
| return DistillationPlan( |
| mode="prompt_pack", |
| teacher_model=current_model, |
| reason=f"{'No GPU available' if not has_gpu else 'Dataset too small for weight updates'} → prompt optimization only", |
| estimated_cost_usd=0.0, |
| estimated_time_hours=0.01, |
| ) |
|
|
| if dataset_size < 1000: |
| return DistillationPlan( |
| mode="lora", |
| teacher_model=current_model, |
| student_base=target_model or current_model, |
| reason=f"Dataset ({dataset_size} examples) suitable for LoRA adapter", |
| estimated_cost_usd=2.0, |
| estimated_time_hours=1.0, |
| acceptance_score=0.9, |
| rollback_model=current_model, |
| ) |
|
|
| return DistillationPlan( |
| mode="distill", |
| teacher_model=current_model, |
| student_base=target_model or "Qwen/Qwen2.5-1.5B-Instruct", |
| reason=f"Large dataset ({dataset_size} examples) → full distillation viable", |
| estimated_cost_usd=10.0, |
| estimated_time_hours=4.0, |
| acceptance_score=0.85, |
| rollback_model=current_model, |
| ) |
|
|