Skip to content

Commit e3b7a31

Browse files
authored
Add files via upload
1 parent f4cd713 commit e3b7a31

9 files changed

Lines changed: 617 additions & 109 deletions

File tree

README.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,21 @@ Training dtype and resource policy:
195195
- Native MLX training pushes UI logs, scalar readouts, and chart points every `log_interval` steps, and Web UI Stop is checked at each batch boundary.
196196
- The Web UI writes a warning-only `<checkpoint>.verify.json` after each stage. It checks no-mask vs all-available MoE parity and, on Apple Silicon, MLX prefill logits against the PyTorch CPU baseline.
197197

198+
Native MLX training is a separate Apple Silicon backend, not `torch.to("mlx")`.
199+
The six-stage MLX trainer mirrors the PyTorch loss stack in `chronos.mlx.*`:
200+
masked CE, DPO/ORPO/GRPO/distillation losses, load balance, temporal locality,
201+
lookahead soft-target supervision, lookahead top-k hit loss, and router KL
202+
anchor. Numerically sensitive pieces run in FP32 even when model weights use
203+
BF16/FP16, which is why `auto` prefers BF16 on MLX/MPS: FP16 has too little
204+
exponent range for router softmax, CE, and Adam moments on small unstable
205+
training runs.
206+
207+
The MLX Web UI trainer reports the same fields as CPU/MPS training:
208+
step/loss/steps-per-second/ETA, checkpoint-save events, stop events, and
209+
verify results. Stop is cooperative at batch boundaries and save intervals
210+
write a valid `.pth` plus sibling `.config.json`, so MLX stages can resume or
211+
feed the PyTorch/export pipeline.
212+
198213
The full six-stage comparison harness lives in `tools/compare_minimind_chronos_v3.py`.
199214

200215
---
@@ -231,6 +246,32 @@ d.describe() # human-readable capability summary
231246

232247
Honest note: upstream PyTorch does not ship a real OpenCL backend, and Vulkan support is still niche. Chronos provides a dispatcher seam so external integrations can plug in cleanly without touching core code.
233248

249+
### MLX lazy/offload runtime
250+
251+
MLX uses Apple unified memory, so Chronos treats "VRAM" and "RAM" as logical
252+
tiers rather than physical buses. The native lazy runtime still enforces the
253+
same offload contract as CUDA/MPS/CPU:
254+
255+
- **Hot slots** hold only the execution-budget experts materialized as live
256+
MLX modules.
257+
- **Warm cache** holds a bounded prediction buffer loaded from `.ctsr`
258+
safetensors, never a hidden copy of every expert.
259+
- **Cold storage** is the checkpoint/export reader or per-expert cluster cache.
260+
In lazy mode, live experts are replaced by placeholders after cache creation;
261+
Chronos does not keep a `_saved_live` full-expert repair cache.
262+
- Lookahead predictions are queued into the warm cache and only promoted when
263+
ready. A true miss synchronously materializes the selected expert only; it
264+
does not full-load the model.
265+
- MLX attention dynamically grows RoPE lookup tables past
266+
`max_position_embeddings`, so long prompts plus decode do not fail at token
267+
257/513/etc.
268+
269+
Inference compare/sweep reports real hot/warm counts, resident hit rate,
270+
prediction hit rate, sync SSD loads, MLX active/cache/peak memory, process RSS,
271+
prefill/decode time, and tokens/sec. A lazy run is not considered
272+
offload-ready unless deterministic exact-lazy output matches full-DRAM output
273+
and fallback weight stays zero.
274+
234275
---
235276

236277
## Hugging Face and vLLM compatibility (M5)
@@ -361,7 +402,7 @@ Tabs included:
361402
- `6-Stage Pipeline` with per-stage dataset paths
362403
- `Inference`
363404
- `Export` for FP16/Q8_0 safetensors and GGUF deployment artifacts
364-
- `Benchmark` with Markdown table + bar plot
405+
- `Benchmark` with Markdown table + comparison plots
365406
- `Auto-Tune` with persistent logs and one-click `Apply Best -> Config`
366407
- `IO Monitor`
367408

README_zh.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,19 @@ flowchart LR
189189
- MLX 原生训练会按 `log_interval` 同步 UI 日志、标量读数和图表点;Web UI Stop 会在每个 batch 边界检查并停止。
190190
- Web UI 每阶段保存后会写出 warning-only 的 `<checkpoint>.verify.json`,检查 no-mask vs all-available MoE 等价性,并在 Apple Silicon 上对比 MLX prefill logits 与 PyTorch CPU baseline。
191191

192+
MLX 训练是 Apple Silicon 原生后端,不是 `torch.to("mlx")`。六阶段 MLX
193+
trainer 在 `chronos.mlx.*` 中复刻 PyTorch 训练栈:masked CE、DPO/ORPO/GRPO/
194+
蒸馏损失、load balance、temporal locality、lookahead soft-target 监督、
195+
lookahead top-k 命中损失、router KL anchor。即使权重使用 BF16/FP16,路由
196+
softmax、CE、Adam moments 等数值敏感路径仍以 FP32 计算;因此 `auto`
197+
MLX/MPS 上默认优先 BF16。FP16 的指数范围太小,在小模型/小 batch/路由分布
198+
剧烈波动时更容易把 loss 推成 NaN。
199+
200+
Web UI 的 MLX 训练日志与 CPU/MPS 对齐:step、loss、steps/s、ETA、
201+
checkpoint 保存事件、stop 事件、verify 结果都会同步到界面。Stop 在 batch
202+
边界协作式生效;定期保存会写出可继续训练的 `.pth` 与同名 `.config.json`
203+
因此 MLX 阶段可以继续接 PyTorch、Export、Diagnose 链路。
204+
192205
完整 6 阶段端到端对比见 `tools/compare_minimind_chronos_v3.py`
193206

194207
---
@@ -224,6 +237,28 @@ d.describe() # 人类可读的能力总览
224237

225238
诚实声明:上游 PyTorch 没有 OpenCL 后端、Vulkan 也仅在自定义构建中可用。Chronos 提供 dispatcher 接缝,使第三方插件无需改核心代码即可接入。
226239

240+
### MLX lazy/offload 运行时
241+
242+
MLX 使用 Apple 统一内存,因此 Chronos 在 MLX 上的 “VRAM/RAM” 是逻辑分层,
243+
不是 PCIe 式物理分层。但 lazy runtime 仍严格遵守与 CUDA/MPS/CPU 一致的
244+
offload contract:
245+
246+
- **Hot slots**:只保存执行预算内、已经 materialize 成 MLX live module 的专家。
247+
- **Warm cache**:只保存有界预测缓冲,从 `.ctsr` safetensors 中按专家/簇加载;
248+
不允许暗中保留全量专家副本。
249+
- **Cold storage**:checkpoint/export reader 或 per-expert cluster cache。Lazy
250+
模式创建 cache 后会把 live experts 替换为 placeholder;不会保留 `_saved_live`
251+
这种全量专家修复缓存。
252+
- Lookahead 预测只进入 warm cache 队列,只有 ready 的专家才会 promote。真实 miss
253+
只同步 materialize 当前被选中的专家,不会 full-load 整个模型。
254+
- MLX attention 会按需扩容 RoPE lookup table,因此 prompt 长度加 decode 超过
255+
`max_position_embeddings` 时不会在第 257/513 等 token 崩溃。
256+
257+
Inference compare/sweep 会报告真实 hot/warm 数量、resident hit、prediction hit、
258+
sync SSD loads、MLX active/cache/peak memory、进程 RSS、prefill/decode 时间与
259+
tokens/s。只有确定性 exact-lazy 输出与 full-DRAM 一致、且 fallback weight 为 0,
260+
才应该把该 checkpoint 标记为 offload-ready。
261+
227262
---
228263

229264
## HuggingFace / vLLM 兼容(M5)
@@ -344,7 +379,7 @@ chronos-ui
344379
python chronos_app.py
345380
```
346381

347-
包含:⚙️ Config(含右侧实时参数估算面板,合并了 Designer)/ 🏋️ Train(拥有 data_path)/ 🧪 6-Stage Pipeline(每阶段独立数据路径)/ 💬 Inference(含懒加载 vs 全量 DRAM 对比)/ 📦 Export(FP16/Q8_0 safetensors 与 GGUF)/ 📊 Benchmark(Markdown 表 + BarPlot)/ 🔬 Auto-Tune(持久化日志 + 一键 Apply Best → Config)/ 📡 IO Monitor。i18n 支持 zh-Hans / zh-Hant / en / ja。
382+
包含:⚙️ Config(含右侧实时参数估算面板,合并了 Designer)/ 🏋️ Train(拥有 data_path)/ 🧪 6-Stage Pipeline(每阶段独立数据路径)/ 💬 Inference(含懒加载 vs 全量 DRAM 对比)/ 📦 Export(FP16/Q8_0 safetensors 与 GGUF)/ 📊 Benchmark(Markdown 表 + 对比图)/ 🔬 Auto-Tune(持久化日志 + 一键 Apply Best → Config)/ 📡 IO Monitor。i18n 支持 zh-Hans / zh-Hant / en / ja。
348383

349384
### 部署导出
350385

chronos/mlx/attention.py

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,39 @@ def _apply_rope(x: mx.array, cos: mx.array, sin: mx.array) -> mx.array:
5959
return (x * c + rotated_half * s).astype(x.dtype)
6060

6161

62-
class MLAAttentionMLX(nn.Module):
62+
class _RopeCacheMixin:
63+
def _init_rope_cache(self, config, dim: int) -> None:
64+
rope_len = int(getattr(config, "max_position_embeddings", 4096))
65+
object.__setattr__(self, "_rope_dim", int(dim))
66+
object.__setattr__(self, "_rope_base", float(getattr(config, "rope_theta", 1_000_000.0)))
67+
object.__setattr__(self, "_rope_scaling", getattr(config, "rope_scaling", None))
68+
cos, sin = _rope_freqs(
69+
int(dim),
70+
max_len=rope_len,
71+
base=self._rope_base,
72+
rope_scaling=self._rope_scaling,
73+
)
74+
object.__setattr__(self, "_cos", cos)
75+
object.__setattr__(self, "_sin", sin)
76+
77+
def _ensure_rope_cache(self, needed_len: int) -> None:
78+
current_len = int(self._cos.shape[0])
79+
if int(needed_len) <= current_len:
80+
return
81+
# Grow geometrically to avoid reallocating every decode step past
82+
# max_position_embeddings.
83+
new_len = max(int(needed_len), current_len * 2)
84+
cos, sin = _rope_freqs(
85+
int(self._rope_dim),
86+
max_len=new_len,
87+
base=float(self._rope_base),
88+
rope_scaling=self._rope_scaling,
89+
)
90+
object.__setattr__(self, "_cos", cos)
91+
object.__setattr__(self, "_sin", sin)
92+
93+
94+
class MLAAttentionMLX(_RopeCacheMixin, nn.Module):
6395
"""
6496
Multi-head Latent Attention for MLX.
6597
@@ -94,17 +126,7 @@ def __init__(self, config):
94126
self.v_proj = nn.Linear(self.kv_latent_dim, self.n_kv_heads * self.head_dim, bias=False)
95127
self.o_proj = nn.Linear(self.n_heads * self.head_dim, H, bias=False)
96128

97-
rope_len = int(getattr(config, "max_position_embeddings", 4096))
98-
cos, sin = _rope_freqs(
99-
self.head_dim,
100-
max_len=rope_len,
101-
base=float(getattr(config, "rope_theta", 1_000_000.0)),
102-
rope_scaling=getattr(config, "rope_scaling", None),
103-
)
104-
# Bypass mlx.nn.Module.__setattr__; these are runtime lookup tables,
105-
# not trainable checkpoint parameters.
106-
object.__setattr__(self, "_cos", cos)
107-
object.__setattr__(self, "_sin", sin)
129+
self._init_rope_cache(config, self.head_dim)
108130

109131
def __call__(
110132
self,
@@ -134,6 +156,7 @@ def __call__(
134156
k_rope_past = None
135157

136158
T_full = c_kv_full.shape[1]
159+
self._ensure_rope_cache(T_full)
137160

138161
# ── Keys / Values (recomputed from cache) ─────────────────
139162
k_nope = self.k_nope_proj(c_kv_full).reshape(B, T_full, self.n_kv_heads, self.nope_dim)
@@ -172,7 +195,7 @@ def __call__(
172195
return self.o_proj(out), new_cache
173196

174197

175-
class SlidingWindowAttentionMLX(nn.Module):
198+
class SlidingWindowAttentionMLX(_RopeCacheMixin, nn.Module):
176199
"""
177200
Standard multi-head attention with a sliding KV window for MLX.
178201
KV cache is capped at window_size tokens — O(1) memory per step.
@@ -193,15 +216,7 @@ def __init__(self, config):
193216
self.q_norm = nn.RMSNorm(self.head_dim, eps=config.rms_norm_eps)
194217
self.k_norm = nn.RMSNorm(self.head_dim, eps=config.rms_norm_eps)
195218

196-
rope_len = int(getattr(config, "max_position_embeddings", 4096))
197-
cos, sin = _rope_freqs(
198-
self.head_dim,
199-
max_len=rope_len,
200-
base=float(getattr(config, "rope_theta", 1_000_000.0)),
201-
rope_scaling=getattr(config, "rope_scaling", None),
202-
)
203-
object.__setattr__(self, "_cos", cos)
204-
object.__setattr__(self, "_sin", sin)
219+
self._init_rope_cache(config, self.head_dim)
205220

206221
def __call__(
207222
self,
@@ -216,6 +231,7 @@ def __call__(
216231
v_new = self.v_proj(x).reshape(B, S, self.n_kv_heads, self.head_dim)
217232

218233
past_len = cache[0].shape[1] if cache is not None else 0
234+
self._ensure_rope_cache(past_len + S)
219235
q = _apply_rope(q, self._cos[past_len:past_len + S], self._sin[past_len:past_len + S])
220236
k_new = _apply_rope(k_new, self._cos[past_len:past_len + S], self._sin[past_len:past_len + S])
221237

0 commit comments

Comments
 (0)