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.
Files changed (83) hide show
  1. chronos/__init__.py +16 -0
  2. chronos/_backend_legacy.py +64 -0
  3. chronos/app.py +122 -0
  4. chronos/backend/__init__.py +40 -0
  5. chronos/backend/dispatcher.py +347 -0
  6. chronos/backend/ext/__init__.py +2 -0
  7. chronos/backend/ext/opencl.py +22 -0
  8. chronos/cli.py +62 -0
  9. chronos/data/__init__.py +0 -0
  10. chronos/data/flexible_dataset.py +309 -0
  11. chronos/deps.py +72 -0
  12. chronos/eval/__init__.py +2 -0
  13. chronos/eval/benchmark.py +159 -0
  14. chronos/eval/io_profiler.py +232 -0
  15. chronos/io/__init__.py +13 -0
  16. chronos/io/async_prefetcher.py +220 -0
  17. chronos/io/cluster_layout.py +317 -0
  18. chronos/io/expert_store.py +559 -0
  19. chronos/io/io_simulator.py +40 -0
  20. chronos/io/storage.py +237 -0
  21. chronos/mlx/__init__.py +32 -0
  22. chronos/mlx/attention.py +230 -0
  23. chronos/mlx/expert_store.py +155 -0
  24. chronos/mlx/inference.py +159 -0
  25. chronos/mlx/model.py +181 -0
  26. chronos/mlx/moe.py +102 -0
  27. chronos/model/__init__.py +6 -0
  28. chronos/model/config.py +54 -0
  29. chronos/model/hf_io.py +149 -0
  30. chronos/model/hybrid_attention.py +237 -0
  31. chronos/model/lookahead_router.py +55 -0
  32. chronos/model/model_chronos.py +271 -0
  33. chronos/model/moe_chronos.py +189 -0
  34. chronos/model/temporal_loss.py +124 -0
  35. chronos/router/__init__.py +12 -0
  36. chronos/router/expert_predictor.py +163 -0
  37. chronos/router/intent_classifier.py +193 -0
  38. chronos/router/prefill_scheduler.py +248 -0
  39. chronos/runtime/__init__.py +2 -0
  40. chronos/runtime/cache_manager.py +117 -0
  41. chronos/runtime/inference_engine.py +295 -0
  42. chronos/runtime/metrics.py +75 -0
  43. chronos/serving/__init__.py +13 -0
  44. chronos/serving/vllm_adapter.py +85 -0
  45. chronos/trainer/__init__.py +6 -0
  46. chronos/trainer/chronos_trainer.py +151 -0
  47. chronos/trainer/distill_trainer.py +193 -0
  48. chronos/trainer/dpo_trainer.py +190 -0
  49. chronos/trainer/grpo_trainer.py +288 -0
  50. chronos/trainer/loss_mixin.py +165 -0
  51. chronos/trainer/optim_utils.py +101 -0
  52. chronos/trainer/orpo_trainer.py +189 -0
  53. chronos/trainer/reward.py +130 -0
  54. chronos/trainer/sft_trainer.py +189 -0
  55. chronos/tuning/__init__.py +1 -0
  56. chronos/tuning/_base_tuner.py +284 -0
  57. chronos/tuning/chronos_auto_tuner.py +203 -0
  58. project_chronos-0.1.1.dist-info/METADATA +670 -0
  59. project_chronos-0.1.1.dist-info/RECORD +83 -0
  60. project_chronos-0.1.1.dist-info/WHEEL +5 -0
  61. project_chronos-0.1.1.dist-info/entry_points.txt +7 -0
  62. project_chronos-0.1.1.dist-info/licenses/LICENSE +177 -0
  63. project_chronos-0.1.1.dist-info/top_level.txt +8 -0
  64. train_chronos.py +133 -0
  65. train_chronos_distill.py +95 -0
  66. train_chronos_dpo.py +82 -0
  67. train_chronos_grpo.py +88 -0
  68. train_chronos_orpo.py +81 -0
  69. train_chronos_sft.py +85 -0
  70. ui/__init__.py +3 -0
  71. ui/estimator.py +204 -0
  72. ui/gradio_compat.py +72 -0
  73. ui/i18n.py +540 -0
  74. ui/presets.py +194 -0
  75. ui/tabs/__init__.py +3 -0
  76. ui/tabs/autotune_tab.py +210 -0
  77. ui/tabs/benchmark_tab.py +196 -0
  78. ui/tabs/config_tab.py +318 -0
  79. ui/tabs/data_tab.py +148 -0
  80. ui/tabs/inference_tab.py +496 -0
  81. ui/tabs/iomon_tab.py +37 -0
  82. ui/tabs/pipeline_tab.py +134 -0
  83. 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
@@ -0,0 +1,3 @@
1
+ """
2
+ ui/__init__.py
3
+ """
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
+