Project-Chronos 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chronos/__init__.py +16 -0
- chronos/_backend_legacy.py +64 -0
- chronos/app.py +122 -0
- chronos/backend/__init__.py +40 -0
- chronos/backend/dispatcher.py +347 -0
- chronos/backend/ext/__init__.py +2 -0
- chronos/backend/ext/opencl.py +22 -0
- chronos/cli.py +62 -0
- chronos/data/__init__.py +0 -0
- chronos/data/flexible_dataset.py +309 -0
- chronos/deps.py +72 -0
- chronos/eval/__init__.py +2 -0
- chronos/eval/benchmark.py +159 -0
- chronos/eval/io_profiler.py +232 -0
- chronos/io/__init__.py +13 -0
- chronos/io/async_prefetcher.py +220 -0
- chronos/io/cluster_layout.py +317 -0
- chronos/io/expert_store.py +559 -0
- chronos/io/io_simulator.py +40 -0
- chronos/io/storage.py +237 -0
- chronos/mlx/__init__.py +32 -0
- chronos/mlx/attention.py +230 -0
- chronos/mlx/expert_store.py +155 -0
- chronos/mlx/inference.py +159 -0
- chronos/mlx/model.py +181 -0
- chronos/mlx/moe.py +102 -0
- chronos/model/__init__.py +6 -0
- chronos/model/config.py +54 -0
- chronos/model/hf_io.py +149 -0
- chronos/model/hybrid_attention.py +237 -0
- chronos/model/lookahead_router.py +55 -0
- chronos/model/model_chronos.py +271 -0
- chronos/model/moe_chronos.py +189 -0
- chronos/model/temporal_loss.py +124 -0
- chronos/router/__init__.py +12 -0
- chronos/router/expert_predictor.py +163 -0
- chronos/router/intent_classifier.py +193 -0
- chronos/router/prefill_scheduler.py +248 -0
- chronos/runtime/__init__.py +2 -0
- chronos/runtime/cache_manager.py +117 -0
- chronos/runtime/inference_engine.py +295 -0
- chronos/runtime/metrics.py +75 -0
- chronos/serving/__init__.py +13 -0
- chronos/serving/vllm_adapter.py +85 -0
- chronos/trainer/__init__.py +6 -0
- chronos/trainer/chronos_trainer.py +151 -0
- chronos/trainer/distill_trainer.py +193 -0
- chronos/trainer/dpo_trainer.py +190 -0
- chronos/trainer/grpo_trainer.py +288 -0
- chronos/trainer/loss_mixin.py +165 -0
- chronos/trainer/optim_utils.py +101 -0
- chronos/trainer/orpo_trainer.py +189 -0
- chronos/trainer/reward.py +130 -0
- chronos/trainer/sft_trainer.py +189 -0
- chronos/tuning/__init__.py +1 -0
- chronos/tuning/_base_tuner.py +284 -0
- chronos/tuning/chronos_auto_tuner.py +203 -0
- project_chronos-0.1.1.dist-info/METADATA +670 -0
- project_chronos-0.1.1.dist-info/RECORD +83 -0
- project_chronos-0.1.1.dist-info/WHEEL +5 -0
- project_chronos-0.1.1.dist-info/entry_points.txt +7 -0
- project_chronos-0.1.1.dist-info/licenses/LICENSE +177 -0
- project_chronos-0.1.1.dist-info/top_level.txt +8 -0
- train_chronos.py +133 -0
- train_chronos_distill.py +95 -0
- train_chronos_dpo.py +82 -0
- train_chronos_grpo.py +88 -0
- train_chronos_orpo.py +81 -0
- train_chronos_sft.py +85 -0
- ui/__init__.py +3 -0
- ui/estimator.py +204 -0
- ui/gradio_compat.py +72 -0
- ui/i18n.py +540 -0
- ui/presets.py +194 -0
- ui/tabs/__init__.py +3 -0
- ui/tabs/autotune_tab.py +210 -0
- ui/tabs/benchmark_tab.py +196 -0
- ui/tabs/config_tab.py +318 -0
- ui/tabs/data_tab.py +148 -0
- ui/tabs/inference_tab.py +496 -0
- ui/tabs/iomon_tab.py +37 -0
- ui/tabs/pipeline_tab.py +134 -0
- ui/tabs/train_tab.py +1688 -0
train_chronos_grpo.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""train_chronos_grpo.py — Stage 5 (GRPO) entry point."""
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import chronos.deps # noqa
|
|
6
|
+
import torch
|
|
7
|
+
from transformers import AutoTokenizer
|
|
8
|
+
|
|
9
|
+
from chronos.backend import resolve_training_device
|
|
10
|
+
from chronos.model.config import ChronosConfig
|
|
11
|
+
from chronos.model.model_chronos import ChronosForCausalLM
|
|
12
|
+
from chronos.trainer.grpo_trainer import ChronosGRPOTrainer, load_grpo_prompts
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
p = argparse.ArgumentParser()
|
|
17
|
+
p.add_argument("--data_path", required=True)
|
|
18
|
+
p.add_argument("--from_weight", default="orpo")
|
|
19
|
+
p.add_argument("--save_dir", default="out")
|
|
20
|
+
p.add_argument("--steps", type=int, default=None)
|
|
21
|
+
p.add_argument("--epochs", type=int, default=1)
|
|
22
|
+
p.add_argument("--max_seq_len", type=int, default=96)
|
|
23
|
+
p.add_argument("--max_gen_len", type=int, default=24)
|
|
24
|
+
p.add_argument("--num_generations", type=int, default=4)
|
|
25
|
+
p.add_argument("--temperature", type=float, default=1.0)
|
|
26
|
+
p.add_argument("--learning_rate", type=float, default=5e-6)
|
|
27
|
+
p.add_argument("--accumulation_steps", type=int, default=1)
|
|
28
|
+
p.add_argument("--grad_clip", type=float, default=1.0)
|
|
29
|
+
p.add_argument("--log_interval", type=int, default=5)
|
|
30
|
+
p.add_argument("--save_interval", type=int, default=10000)
|
|
31
|
+
p.add_argument("--device", default="auto")
|
|
32
|
+
p.add_argument("--dtype", default="float16")
|
|
33
|
+
p.add_argument("--hidden_size", type=int, default=256)
|
|
34
|
+
p.add_argument("--num_hidden_layers", type=int, default=4)
|
|
35
|
+
p.add_argument("--num_experts", type=int, default=4)
|
|
36
|
+
p.add_argument("--beta", type=float, default=0.04, help="KL penalty weight")
|
|
37
|
+
p.add_argument("--lambda_router_anchor", type=float, default=0.1)
|
|
38
|
+
p.add_argument("--reward", type=str, default="toy",
|
|
39
|
+
help="Reward spec: 'toy' (default) or 'lm:/path/to/reward-model'.")
|
|
40
|
+
args = p.parse_args()
|
|
41
|
+
selected_backend, resolved_device = resolve_training_device(args.device)
|
|
42
|
+
args.device = resolved_device
|
|
43
|
+
|
|
44
|
+
cfg = ChronosConfig(
|
|
45
|
+
hidden_size=args.hidden_size,
|
|
46
|
+
num_hidden_layers=args.num_hidden_layers,
|
|
47
|
+
num_experts=args.num_experts,
|
|
48
|
+
num_experts_per_tok=1,
|
|
49
|
+
max_position_embeddings=args.max_seq_len + args.max_gen_len,
|
|
50
|
+
flash_attn=False,
|
|
51
|
+
lambda_router_anchor=args.lambda_router_anchor,
|
|
52
|
+
)
|
|
53
|
+
tokenizer = AutoTokenizer.from_pretrained(chronos.deps.get_tokenizer_path())
|
|
54
|
+
model = ChronosForCausalLM(cfg).to(args.device)
|
|
55
|
+
|
|
56
|
+
ckp_path = os.path.join(args.save_dir, f"{args.from_weight}_{cfg.hidden_size}_moe.pth")
|
|
57
|
+
if os.path.exists(ckp_path):
|
|
58
|
+
state = torch.load(ckp_path, map_location=args.device)
|
|
59
|
+
model.load_state_dict(state, strict=False)
|
|
60
|
+
print(f"[GRPO] Loaded {ckp_path}")
|
|
61
|
+
else:
|
|
62
|
+
print(f"[GRPO] No checkpoint at {ckp_path} — training from random init.")
|
|
63
|
+
print(f"[GRPO] Training backend: {selected_backend} device={args.device}")
|
|
64
|
+
|
|
65
|
+
prompts = load_grpo_prompts(args.data_path, max_prompts=args.steps)
|
|
66
|
+
from chronos.trainer.reward import build_reward_fn
|
|
67
|
+
reward_fn = build_reward_fn(args.reward)
|
|
68
|
+
trainer = ChronosGRPOTrainer(model, cfg, args, tokenizer, reward_fn=reward_fn)
|
|
69
|
+
|
|
70
|
+
if cfg.lambda_router_anchor > 0 and prompts:
|
|
71
|
+
# Build a calibration batch from the first prompt
|
|
72
|
+
ptext = prompts[0]
|
|
73
|
+
calib = tokenizer(ptext, return_tensors="pt", truncation=True,
|
|
74
|
+
max_length=args.max_seq_len).input_ids
|
|
75
|
+
trainer.set_calibration_batch(calib)
|
|
76
|
+
print("[GRPO] Router anchor reference captured.")
|
|
77
|
+
|
|
78
|
+
iters = len(prompts) if args.steps is None else min(args.steps, len(prompts))
|
|
79
|
+
for epoch in range(args.epochs):
|
|
80
|
+
trainer.train_epoch(epoch, prompts, iters, max_steps=args.steps)
|
|
81
|
+
if args.steps is not None:
|
|
82
|
+
break
|
|
83
|
+
trainer._save(epoch=args.epochs - 1, step=iters)
|
|
84
|
+
print(f"[GRPO] Done. Checkpoint: {args.save_dir}/grpo_{cfg.hidden_size}_moe.pth")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
main()
|
train_chronos_orpo.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""train_chronos_orpo.py — Stage 4 (ORPO) entry point."""
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import chronos.deps # noqa
|
|
6
|
+
import torch
|
|
7
|
+
from transformers import AutoTokenizer
|
|
8
|
+
|
|
9
|
+
from chronos.backend import resolve_training_device
|
|
10
|
+
from chronos.model.config import ChronosConfig
|
|
11
|
+
from chronos.model.model_chronos import ChronosForCausalLM
|
|
12
|
+
from chronos.trainer.orpo_trainer import ChronosORPOTrainer, build_orpo_loader
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
p = argparse.ArgumentParser()
|
|
17
|
+
p.add_argument("--data_path", required=True)
|
|
18
|
+
p.add_argument("--from_weight", default="sft")
|
|
19
|
+
p.add_argument("--save_dir", default="out")
|
|
20
|
+
p.add_argument("--steps", type=int, default=None)
|
|
21
|
+
p.add_argument("--epochs", type=int, default=1)
|
|
22
|
+
p.add_argument("--batch_size", type=int, default=2)
|
|
23
|
+
p.add_argument("--max_seq_len", type=int, default=128)
|
|
24
|
+
p.add_argument("--learning_rate", type=float, default=5e-6)
|
|
25
|
+
p.add_argument("--accumulation_steps", type=int, default=1)
|
|
26
|
+
p.add_argument("--grad_clip", type=float, default=1.0)
|
|
27
|
+
p.add_argument("--log_interval", type=int, default=10)
|
|
28
|
+
p.add_argument("--save_interval", type=int, default=10000)
|
|
29
|
+
p.add_argument("--device", default="auto")
|
|
30
|
+
p.add_argument("--dtype", default="float16")
|
|
31
|
+
p.add_argument("--hidden_size", type=int, default=256)
|
|
32
|
+
p.add_argument("--num_hidden_layers", type=int, default=4)
|
|
33
|
+
p.add_argument("--num_experts", type=int, default=4)
|
|
34
|
+
p.add_argument("--beta", type=float, default=0.1)
|
|
35
|
+
p.add_argument("--lambda_or", type=float, default=0.1)
|
|
36
|
+
p.add_argument("--lambda_router_anchor", type=float, default=0.1)
|
|
37
|
+
args = p.parse_args()
|
|
38
|
+
selected_backend, resolved_device = resolve_training_device(args.device)
|
|
39
|
+
args.device = resolved_device
|
|
40
|
+
|
|
41
|
+
cfg = ChronosConfig(
|
|
42
|
+
hidden_size=args.hidden_size,
|
|
43
|
+
num_hidden_layers=args.num_hidden_layers,
|
|
44
|
+
num_experts=args.num_experts,
|
|
45
|
+
num_experts_per_tok=1,
|
|
46
|
+
max_position_embeddings=args.max_seq_len,
|
|
47
|
+
flash_attn=False,
|
|
48
|
+
lambda_router_anchor=args.lambda_router_anchor,
|
|
49
|
+
)
|
|
50
|
+
tokenizer = AutoTokenizer.from_pretrained(chronos.deps.get_tokenizer_path())
|
|
51
|
+
model = ChronosForCausalLM(cfg).to(args.device)
|
|
52
|
+
|
|
53
|
+
ckp_path = os.path.join(args.save_dir, f"{args.from_weight}_{cfg.hidden_size}_moe.pth")
|
|
54
|
+
if os.path.exists(ckp_path):
|
|
55
|
+
state = torch.load(ckp_path, map_location=args.device)
|
|
56
|
+
model.load_state_dict(state, strict=False)
|
|
57
|
+
print(f"[ORPO] Loaded {ckp_path}")
|
|
58
|
+
else:
|
|
59
|
+
print(f"[ORPO] No checkpoint at {ckp_path} — training from random init.")
|
|
60
|
+
print(f"[ORPO] Training backend: {selected_backend} device={args.device}")
|
|
61
|
+
|
|
62
|
+
loader = build_orpo_loader(args.data_path, tokenizer, args.max_seq_len, args.batch_size)
|
|
63
|
+
trainer = ChronosORPOTrainer(model, cfg, args, tokenizer)
|
|
64
|
+
|
|
65
|
+
if cfg.lambda_router_anchor > 0:
|
|
66
|
+
first = next(iter(loader))
|
|
67
|
+
calib = torch.cat([first["x_chosen"], first["x_rejected"]], dim=0)
|
|
68
|
+
trainer.set_calibration_batch(calib)
|
|
69
|
+
print("[ORPO] Router anchor reference captured.")
|
|
70
|
+
|
|
71
|
+
iters = len(loader) if args.steps is None else min(args.steps, len(loader))
|
|
72
|
+
for epoch in range(args.epochs):
|
|
73
|
+
trainer.train_epoch(epoch, loader, iters, max_steps=args.steps)
|
|
74
|
+
if args.steps is not None:
|
|
75
|
+
break
|
|
76
|
+
trainer._save(epoch=args.epochs - 1, step=iters)
|
|
77
|
+
print(f"[ORPO] Done. Checkpoint: {args.save_dir}/orpo_{cfg.hidden_size}_moe.pth")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
main()
|
train_chronos_sft.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""train_chronos_sft.py — Stage 2 (SFT) entry point."""
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import chronos.deps # noqa
|
|
7
|
+
import torch
|
|
8
|
+
from torch.utils.data import DataLoader
|
|
9
|
+
from transformers import AutoTokenizer
|
|
10
|
+
|
|
11
|
+
from chronos.backend import resolve_training_device
|
|
12
|
+
from chronos.model.config import ChronosConfig
|
|
13
|
+
from chronos.model.model_chronos import ChronosForCausalLM
|
|
14
|
+
from chronos.trainer.sft_trainer import ChronosSFTTrainer, build_sft_loader
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main():
|
|
18
|
+
p = argparse.ArgumentParser()
|
|
19
|
+
p.add_argument("--data_path", required=True)
|
|
20
|
+
p.add_argument("--from_weight", default="chronos",
|
|
21
|
+
help="Pretrained checkpoint stem (loaded from save_dir/<stem>_<H>_moe.pth).")
|
|
22
|
+
p.add_argument("--save_dir", default="out")
|
|
23
|
+
p.add_argument("--steps", type=int, default=None,
|
|
24
|
+
help="If set, stop after N steps (smoke runs).")
|
|
25
|
+
p.add_argument("--epochs", type=int, default=1)
|
|
26
|
+
p.add_argument("--batch_size", type=int, default=2)
|
|
27
|
+
p.add_argument("--max_seq_len", type=int, default=128)
|
|
28
|
+
p.add_argument("--learning_rate", type=float, default=1e-4)
|
|
29
|
+
p.add_argument("--accumulation_steps", type=int, default=1)
|
|
30
|
+
p.add_argument("--grad_clip", type=float, default=1.0)
|
|
31
|
+
p.add_argument("--log_interval", type=int, default=10)
|
|
32
|
+
p.add_argument("--save_interval", type=int, default=10000)
|
|
33
|
+
p.add_argument("--device", default="auto")
|
|
34
|
+
p.add_argument("--dtype", default="float16")
|
|
35
|
+
p.add_argument("--hidden_size", type=int, default=256)
|
|
36
|
+
p.add_argument("--num_hidden_layers", type=int, default=4)
|
|
37
|
+
p.add_argument("--num_experts", type=int, default=4)
|
|
38
|
+
p.add_argument("--lambda_router_anchor", type=float, default=0.01,
|
|
39
|
+
help="SFT default: weak anchor.")
|
|
40
|
+
args = p.parse_args()
|
|
41
|
+
selected_backend, resolved_device = resolve_training_device(args.device)
|
|
42
|
+
args.device = resolved_device
|
|
43
|
+
|
|
44
|
+
cfg = ChronosConfig(
|
|
45
|
+
hidden_size=args.hidden_size,
|
|
46
|
+
num_hidden_layers=args.num_hidden_layers,
|
|
47
|
+
num_experts=args.num_experts,
|
|
48
|
+
num_experts_per_tok=1,
|
|
49
|
+
max_position_embeddings=args.max_seq_len,
|
|
50
|
+
flash_attn=False,
|
|
51
|
+
lambda_router_anchor=args.lambda_router_anchor,
|
|
52
|
+
)
|
|
53
|
+
tokenizer = AutoTokenizer.from_pretrained(chronos.deps.get_tokenizer_path())
|
|
54
|
+
model = ChronosForCausalLM(cfg).to(args.device)
|
|
55
|
+
|
|
56
|
+
# Load pretrain checkpoint if present
|
|
57
|
+
ckp_path = os.path.join(args.save_dir, f"{args.from_weight}_{cfg.hidden_size}_moe.pth")
|
|
58
|
+
if os.path.exists(ckp_path):
|
|
59
|
+
state = torch.load(ckp_path, map_location=args.device)
|
|
60
|
+
model.load_state_dict(state, strict=False)
|
|
61
|
+
print(f"[SFT] Loaded pretrain weights from {ckp_path}")
|
|
62
|
+
else:
|
|
63
|
+
print(f"[SFT] No pretrain checkpoint at {ckp_path} — training from random init.")
|
|
64
|
+
print(f"[SFT] Training backend: {selected_backend} device={args.device}")
|
|
65
|
+
|
|
66
|
+
loader = build_sft_loader(args.data_path, tokenizer, args.max_seq_len, args.batch_size)
|
|
67
|
+
trainer = ChronosSFTTrainer(model, cfg, args, tokenizer)
|
|
68
|
+
|
|
69
|
+
# Capture router reference on the first batch BEFORE training
|
|
70
|
+
if cfg.lambda_router_anchor > 0:
|
|
71
|
+
first_batch = next(iter(loader))
|
|
72
|
+
trainer.set_calibration_batch(first_batch[0])
|
|
73
|
+
print("[SFT] Router anchor reference captured.")
|
|
74
|
+
|
|
75
|
+
iters = len(loader) if args.steps is None else min(args.steps, len(loader))
|
|
76
|
+
for epoch in range(args.epochs):
|
|
77
|
+
trainer.train_epoch(epoch, loader, iters, max_steps=args.steps)
|
|
78
|
+
if args.steps is not None:
|
|
79
|
+
break
|
|
80
|
+
trainer._save(epoch=args.epochs - 1, step=iters)
|
|
81
|
+
print(f"[SFT] Done. Checkpoint: {args.save_dir}/sft_{cfg.hidden_size}_moe.pth")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
main()
|
ui/__init__.py
ADDED
ui/estimator.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ui/estimator.py
|
|
3
|
+
|
|
4
|
+
Parameter-count and memory-footprint estimator for the Architecture
|
|
5
|
+
Designer tab. Pure-math functions; no torch dependency.
|
|
6
|
+
|
|
7
|
+
The numbers match an actual ``ChronosForCausalLM`` state_dict to within
|
|
8
|
+
~1% for standard configs — the mismatch comes from bias terms and RMSNorm
|
|
9
|
+
scale vectors which are tiny relative to the weight matrices we count.
|
|
10
|
+
|
|
11
|
+
IMPORTANT: MiniMindConfig auto-derives ``intermediate_size`` from
|
|
12
|
+
``hidden_size`` as ``ceil(H·π/64)·64`` unless the user explicitly sets
|
|
13
|
+
it, and ``moe_intermediate_size`` defaults to ``intermediate_size``.
|
|
14
|
+
The estimator replicates that auto-derivation here so the UI preview
|
|
15
|
+
matches what the trainer actually builds. Callers can still override
|
|
16
|
+
either value by passing it into ``ArchConfig`` explicitly.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import math
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Dict, Optional
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
DTYPE_BYTES = {
|
|
26
|
+
"fp32": 4, "float32": 4,
|
|
27
|
+
"bf16": 2, "bfloat16": 2,
|
|
28
|
+
"fp16": 2, "float16": 2,
|
|
29
|
+
"int8": 1,
|
|
30
|
+
"nf4": 0.5,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _minimind_intermediate(hidden_size: int) -> int:
|
|
35
|
+
"""Replicates MiniMindConfig's auto-derivation rule."""
|
|
36
|
+
return int(math.ceil(hidden_size * math.pi / 64) * 64)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ArchConfig:
|
|
41
|
+
hidden_size: int = 256
|
|
42
|
+
num_hidden_layers: int = 4
|
|
43
|
+
num_attention_heads: int = 8
|
|
44
|
+
num_key_value_heads: int = 4
|
|
45
|
+
vocab_size: int = 6400
|
|
46
|
+
# intermediate_size / moe_intermediate_size: when None, auto-derive from
|
|
47
|
+
# hidden_size via the MiniMind rule so the estimate matches the trainer.
|
|
48
|
+
intermediate_size: Optional[int] = None
|
|
49
|
+
moe_intermediate_size: Optional[int] = None
|
|
50
|
+
num_experts: int = 4
|
|
51
|
+
num_experts_per_tok: int = 1
|
|
52
|
+
num_shared_experts: int = 1
|
|
53
|
+
max_position_embeddings: int = 2048
|
|
54
|
+
lookahead_steps: int = 2
|
|
55
|
+
kv_latent_dim: int = 64
|
|
56
|
+
rope_dim: int = 32
|
|
57
|
+
sliding_window_size: int = 2048
|
|
58
|
+
use_hybrid_attention: bool = True
|
|
59
|
+
tie_word_embeddings: bool = True
|
|
60
|
+
dtype: str = "fp16"
|
|
61
|
+
|
|
62
|
+
def __post_init__(self):
|
|
63
|
+
auto = _minimind_intermediate(self.hidden_size)
|
|
64
|
+
if self.intermediate_size is None or self.intermediate_size <= 0:
|
|
65
|
+
self.intermediate_size = auto
|
|
66
|
+
if self.moe_intermediate_size is None or self.moe_intermediate_size <= 0:
|
|
67
|
+
self.moe_intermediate_size = auto
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _attn_params(cfg: ArchConfig) -> int:
|
|
71
|
+
"""Params per attention block (MLA/SW hybrid has similar Q/K/V/O sizes)."""
|
|
72
|
+
h = cfg.hidden_size
|
|
73
|
+
n_h = cfg.num_attention_heads
|
|
74
|
+
n_kv = cfg.num_key_value_heads
|
|
75
|
+
head_dim = h // n_h
|
|
76
|
+
q = h * (n_h * head_dim)
|
|
77
|
+
k = h * (n_kv * head_dim)
|
|
78
|
+
v = h * (n_kv * head_dim)
|
|
79
|
+
o = (n_h * head_dim) * h
|
|
80
|
+
if cfg.use_hybrid_attention:
|
|
81
|
+
# MLA adds latent KV projections; negligible vs full KV projections.
|
|
82
|
+
q += h * cfg.kv_latent_dim
|
|
83
|
+
k += cfg.kv_latent_dim * (n_kv * head_dim)
|
|
84
|
+
v += cfg.kv_latent_dim * (n_kv * head_dim)
|
|
85
|
+
return q + k + v + o
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _expert_params(cfg: ArchConfig) -> int:
|
|
89
|
+
"""Params per single expert FeedForward (SwiGLU: gate+up+down)."""
|
|
90
|
+
h = cfg.hidden_size
|
|
91
|
+
d = cfg.moe_intermediate_size
|
|
92
|
+
return 3 * h * d
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _per_layer_params(cfg: ArchConfig) -> int:
|
|
96
|
+
"""Attention + routing + all experts + shared experts + 2×RMSNorm."""
|
|
97
|
+
attn = _attn_params(cfg)
|
|
98
|
+
router = cfg.hidden_size * cfg.num_experts
|
|
99
|
+
experts = cfg.num_experts * _expert_params(cfg)
|
|
100
|
+
shared = cfg.num_shared_experts * _expert_params(cfg)
|
|
101
|
+
rmsnorm = 2 * cfg.hidden_size # 2 norms per block
|
|
102
|
+
return attn + router + experts + shared + rmsnorm
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _active_expert_slots(cfg: ArchConfig) -> int:
|
|
106
|
+
"""Experts whose weights are *used* on a given token: top-k + shared."""
|
|
107
|
+
return cfg.num_experts_per_tok + cfg.num_shared_experts
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def total_params(cfg: ArchConfig) -> int:
|
|
111
|
+
"""Full model parameter count."""
|
|
112
|
+
embed = cfg.vocab_size * cfg.hidden_size
|
|
113
|
+
layers = cfg.num_hidden_layers * _per_layer_params(cfg)
|
|
114
|
+
lookahead = (
|
|
115
|
+
cfg.hidden_size * (cfg.hidden_size // 4)
|
|
116
|
+
+ (cfg.hidden_size // 4) * cfg.num_experts * (cfg.lookahead_steps + 1)
|
|
117
|
+
)
|
|
118
|
+
lm_head = 0 if cfg.tie_word_embeddings else embed
|
|
119
|
+
return int(embed + layers + lookahead + lm_head + cfg.hidden_size) # + final norm
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def active_params(cfg: ArchConfig) -> int:
|
|
123
|
+
"""Params touched by one token: embed + L·(attn + norm + (topK+shared)·expert)."""
|
|
124
|
+
embed = cfg.vocab_size * cfg.hidden_size
|
|
125
|
+
per_layer_active = (
|
|
126
|
+
_attn_params(cfg)
|
|
127
|
+
+ 2 * cfg.hidden_size
|
|
128
|
+
+ cfg.hidden_size * cfg.num_experts # router always active
|
|
129
|
+
+ _active_expert_slots(cfg) * _expert_params(cfg)
|
|
130
|
+
)
|
|
131
|
+
lm_head_active = 0 if cfg.tie_word_embeddings else embed
|
|
132
|
+
return int(embed + cfg.num_hidden_layers * per_layer_active + lm_head_active)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def kv_cache_bytes(cfg: ArchConfig, seq_len: int = 1024) -> int:
|
|
136
|
+
"""Approximate KV-cache bytes for a given sequence length."""
|
|
137
|
+
h = cfg.hidden_size
|
|
138
|
+
n_h = cfg.num_attention_heads
|
|
139
|
+
n_kv = cfg.num_key_value_heads
|
|
140
|
+
head_dim = h // n_h
|
|
141
|
+
bytes_per = DTYPE_BYTES.get(cfg.dtype, 2)
|
|
142
|
+
if cfg.use_hybrid_attention:
|
|
143
|
+
# Half the layers use MLA (compressed), half use sliding window.
|
|
144
|
+
# MLA cache ≈ kv_latent_dim per position; SW cache capped at window.
|
|
145
|
+
mla_layers = cfg.num_hidden_layers // 2
|
|
146
|
+
sw_layers = cfg.num_hidden_layers - mla_layers
|
|
147
|
+
mla_bytes = mla_layers * seq_len * cfg.kv_latent_dim * bytes_per
|
|
148
|
+
sw_bytes = sw_layers * min(seq_len, cfg.sliding_window_size) * 2 * n_kv * head_dim * bytes_per
|
|
149
|
+
return int(mla_bytes + sw_bytes)
|
|
150
|
+
return int(cfg.num_hidden_layers * seq_len * 2 * n_kv * head_dim * bytes_per)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def memory_footprint(cfg: ArchConfig, seq_len: int = 1024) -> Dict[str, float]:
|
|
154
|
+
"""Return a dict of byte sizes for display in the UI."""
|
|
155
|
+
bytes_per = DTYPE_BYTES.get(cfg.dtype, 2)
|
|
156
|
+
total_b = total_params(cfg) * bytes_per
|
|
157
|
+
active_b = active_params(cfg) * bytes_per
|
|
158
|
+
kv_b = kv_cache_bytes(cfg, seq_len=seq_len)
|
|
159
|
+
cold_b = total_b - active_b
|
|
160
|
+
return {
|
|
161
|
+
"total_bytes": total_b,
|
|
162
|
+
"active_bytes": active_b,
|
|
163
|
+
"cold_bytes": cold_b, # candidates for SSD offload
|
|
164
|
+
"kv_cache_bytes": kv_b,
|
|
165
|
+
"vram_estimate_bytes": active_b + kv_b,
|
|
166
|
+
"ssd_estimate_bytes": cold_b,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def estimated_decode_tps(cfg: ArchConfig, hit_rate: float = 0.85,
|
|
171
|
+
ssd_gbps: float = 3.0, vram_gbps: float = 600.0) -> float:
|
|
172
|
+
"""Order-of-magnitude decode throughput estimator.
|
|
173
|
+
|
|
174
|
+
Model: tps ≈ 1 / (t_compute + t_miss).
|
|
175
|
+
t_compute ≈ active_params · flop_per_param / compute_gflops (simplified
|
|
176
|
+
to a linear-in-active-params term with 1e-10 coefficient
|
|
177
|
+
so the output lives in a sensible range for toy models).
|
|
178
|
+
t_miss ≈ (1 − hit_rate) · expert_bytes_per_layer · num_layers / ssd_gbps.
|
|
179
|
+
Conservative; intended as a UI hint, not a benchmark.
|
|
180
|
+
"""
|
|
181
|
+
bytes_per = DTYPE_BYTES.get(cfg.dtype, 2)
|
|
182
|
+
active_b = active_params(cfg) * bytes_per
|
|
183
|
+
t_compute = max(active_b / (vram_gbps * 1e9), 1e-6)
|
|
184
|
+
expert_b = _expert_params(cfg) * bytes_per
|
|
185
|
+
t_miss = (1.0 - hit_rate) * expert_b * cfg.num_hidden_layers / (ssd_gbps * 1e9)
|
|
186
|
+
return 1.0 / (t_compute + t_miss)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def fmt_bytes(n: int) -> str:
|
|
190
|
+
for unit in ("B", "KB", "MB", "GB", "TB"):
|
|
191
|
+
if abs(n) < 1024:
|
|
192
|
+
return f"{n:.2f} {unit}"
|
|
193
|
+
n /= 1024
|
|
194
|
+
return f"{n:.2f} PB"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def fmt_params(n: int) -> str:
|
|
198
|
+
if n >= 1e9:
|
|
199
|
+
return f"{n/1e9:.2f} B"
|
|
200
|
+
if n >= 1e6:
|
|
201
|
+
return f"{n/1e6:.2f} M"
|
|
202
|
+
if n >= 1e3:
|
|
203
|
+
return f"{n/1e3:.2f} K"
|
|
204
|
+
return str(n)
|
ui/gradio_compat.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Optional Gradio compatibility layer.
|
|
2
|
+
|
|
3
|
+
The core smoke tests exercise helper functions from the UI modules in minimal
|
|
4
|
+
CI environments where the optional WebUI dependency may not be installed. In a
|
|
5
|
+
real WebUI install this module simply re-exports gradio. Without gradio it
|
|
6
|
+
provides a tiny no-op component surface so imports and build_app() smoke tests
|
|
7
|
+
still validate the wiring that does not require a browser runtime.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from types import SimpleNamespace
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
try: # pragma: no cover - exercised when gradio is installed
|
|
15
|
+
import gradio as gr # type: ignore
|
|
16
|
+
HAS_GRADIO = True
|
|
17
|
+
except ModuleNotFoundError: # pragma: no cover - exercised in slim CI
|
|
18
|
+
HAS_GRADIO = False
|
|
19
|
+
|
|
20
|
+
class _Component:
|
|
21
|
+
def __init__(self, *args, **kwargs):
|
|
22
|
+
self.args = args
|
|
23
|
+
self.kwargs = kwargs
|
|
24
|
+
self.value = kwargs.get("value")
|
|
25
|
+
self.label = kwargs.get("label")
|
|
26
|
+
|
|
27
|
+
def __enter__(self):
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
def __exit__(self, exc_type, exc, tb):
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
def click(self, *args, **kwargs):
|
|
34
|
+
return self
|
|
35
|
+
|
|
36
|
+
def change(self, *args, **kwargs):
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
def tick(self, *args, **kwargs):
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def launch(self, **kwargs):
|
|
43
|
+
self.launch_kwargs = kwargs
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def _update(**kwargs):
|
|
47
|
+
return SimpleNamespace(**kwargs)
|
|
48
|
+
|
|
49
|
+
gr = SimpleNamespace(
|
|
50
|
+
Accordion=_Component,
|
|
51
|
+
BarPlot=_Component,
|
|
52
|
+
Blocks=_Component,
|
|
53
|
+
Button=_Component,
|
|
54
|
+
Checkbox=_Component,
|
|
55
|
+
Column=_Component,
|
|
56
|
+
Dropdown=_Component,
|
|
57
|
+
File=_Component,
|
|
58
|
+
HTML=_Component,
|
|
59
|
+
JSON=_Component,
|
|
60
|
+
Markdown=_Component,
|
|
61
|
+
Number=_Component,
|
|
62
|
+
Plot=_Component,
|
|
63
|
+
Radio=_Component,
|
|
64
|
+
Row=_Component,
|
|
65
|
+
Slider=_Component,
|
|
66
|
+
State=_Component,
|
|
67
|
+
Tab=_Component,
|
|
68
|
+
Textbox=_Component,
|
|
69
|
+
Timer=_Component,
|
|
70
|
+
update=_update,
|
|
71
|
+
)
|
|
72
|
+
|