File size: 5,433 Bytes
2fb10fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"""runtime/memory_manager.py — Milestone 4: Memory Manager (PM v0.2 §5)。

职责: 基于 mac16.yaml 的 memory_budget, 动态决定 load/unload/evict。
当前阶段驻留管线的决策模型是确定性的 (AR → unload → Flow), 本模块把
"决策" 从管线代码中抽出来, 并提供真实内存遥测的校验闭环:
  1. 读取 profile 的 memory_budget_breakdown
  2. 决策器: 根据 active/cache/峰值与预算判断某阶段是否可以驻留
  3. 阶段会话: enter → materialize, exit → 释放并校验回落
  4. 测量: 每阶段峰值/耗时/缓存归零检查, 输出 memory report

后续 (M4 扩展) 可加:
  - 动态 eviction: 当 Flow 阶段 chunk 峰值超预算时, 降低 chunk 帧数
  - 双预算档位: preview (更激进) vs create (更保守)
"""
from __future__ import annotations

import gc
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable, Optional

import mlx.core as mx
import yaml

from runtime.profiler import gb


@dataclass
class StageDecision:
    """一次 load/unload 决策的记录。"""

    stage: str
    action: str  # load | unload | skip
    weight_gb: float
    active_before_gb: float
    active_after_gb: float
    peak_gb: float
    budget_ok: bool
    cache_cleared: bool
    note: str = ""


@dataclass
class MemoryReport:
    stages: list[StageDecision] = field(default_factory=list)
    max_peak_gb: float = 0.0
    profile_name: str = "?"

    def add(self, d: StageDecision) -> None:
        self.stages.append(d)
        self.max_peak_gb = max(self.max_peak_gb, d.peak_gb)

    def to_dict(self) -> dict:
        max_peak_gb = max(
            (stage.peak_gb for stage in self.stages), default=self.max_peak_gb
        )
        return {
            "profile": self.profile_name,
            "max_peak_gb": round(max_peak_gb, 3),
            "stages": [
                {
                    "stage": s.stage,
                    "action": s.action,
                    "weight_gb": round(s.weight_gb, 3),
                    "active_before_gb": round(s.active_before_gb, 3),
                    "active_after_gb": round(s.active_after_gb, 3),
                    "peak_gb": round(s.peak_gb, 3),
                    "budget_ok": s.budget_ok,
                    "cache_cleared": s.cache_cleared,
                }
                for s in self.stages
            ],
        }


class MemoryManager:
    """基于 profile 预算的确定性阶段决策器 + 遥测。"""

    def __init__(
        self,
        profile_path: Optional[Path] = None,
        budget_gb: Optional[float] = None,
    ) -> None:
        self.profile = self._load_profile(profile_path)
        self.budget_gb = budget_gb or float(self.profile.get("memory_budget_gb", 11.0))

    # -------------------------------------------------- 公共 API
    def can_resident(self, weight_gb: float, current_active_gb: float) -> bool:
        """决策: 当前 active + 待加载权重是否在预算内 (留 10% 余量)。"""
        return current_active_gb + weight_gb <= self.budget_gb * 0.9

    def stage_enter(self, stage: str, weight_gb: float, loader: Callable):
        """进入阶段: 检查预算 → 执行加载 → 记录峰值。返回加载的模型。"""
        before = gb(mx.get_active_memory())
        ok = self.can_resident(weight_gb, before)
        if not ok:
            mx.clear_cache()
            before = gb(mx.get_active_memory())
            ok = self.can_resident(weight_gb, before)
        if not ok:
            raise MemoryError(
                f"阶段 {stage} 需要 {weight_gb:.2f}GB,但当前预算 {self.budget_gb:.2f}GB 不足"
            )
        mx.reset_peak_memory()
        obj = loader()
        mx.synchronize()
        peak = gb(mx.get_peak_memory())
        after = gb(mx.get_active_memory())
        return obj, StageDecision(
            stage=stage,
            action="load" if ok else "load_after_evict",
            weight_gb=weight_gb,
            active_before_gb=before,
            active_after_gb=after,
            peak_gb=peak,
            budget_ok=ok,
            cache_cleared=not ok,
        )

    def stage_exit(
        self,
        decision: StageDecision,
        release: Callable[[], None] | None = None,
    ) -> None:
        """退出阶段: 先清掉调用方持有的模型引用, 再执行释放四步曲。"""
        if release is not None:
            release()
        gc.collect()
        mx.synchronize()
        mx.clear_cache()
        cache = gb(mx.get_cache_memory())
        decision.active_after_gb = gb(mx.get_active_memory())
        decision.cache_cleared = cache == 0.0

    def report(self) -> MemoryReport:
        r = MemoryReport(profile_name=self.profile.get("device", "?"))
        return r

    # -------------------------------------------------- helpers
    @staticmethod
    def _load_profile(profile_path: Optional[Path]) -> dict:
        if profile_path is None:
            default = Path(__file__).resolve().parent.parent / "profiles" / "mac16.yaml"
            profile_path = default
        if not profile_path.exists():
            return {"memory_budget_gb": 11.0, "device": "unknown"}
        with open(profile_path) as f:
            return yaml.safe_load(f) or {}


def current_usage_gb() -> float:
    return gb(mx.get_active_memory())


def current_peak_gb() -> float:
    return gb(mx.get_peak_memory())