File size: 4,547 Bytes
2535843
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""
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     # Candidate must achieve this on eval
    rollback_model: str = ""          # What to revert to if candidate fails
    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,
    )