flexsds 0.2.0__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 (62) hide show
  1. flexsds/__init__.py +4 -0
  2. flexsds/assets/ptit_logo.png +0 -0
  3. flexsds/cli/__init__.py +1 -0
  4. flexsds/cli/banner.py +84 -0
  5. flexsds/cli/main.py +330 -0
  6. flexsds/cli/output.py +69 -0
  7. flexsds/cli/task_runner.py +171 -0
  8. flexsds/cli/wizard.py +360 -0
  9. flexsds/client/__init__.py +4 -0
  10. flexsds/client/chat.py +137 -0
  11. flexsds/client/client.py +107 -0
  12. flexsds/client/monitor.py +95 -0
  13. flexsds/core/__init__.py +44 -0
  14. flexsds/core/compatibility.py +442 -0
  15. flexsds/core/controller.py +502 -0
  16. flexsds/core/cost_model.py +530 -0
  17. flexsds/core/credentials.py +111 -0
  18. flexsds/core/hardware.py +77 -0
  19. flexsds/core/tree_topology.py +296 -0
  20. flexsds/profiles/profile_sm75_t4.json +62 -0
  21. flexsds/profiles/profile_sm89_l4.json +194 -0
  22. flexsds/server/__init__.py +1 -0
  23. flexsds/server/colab_runner.py +62 -0
  24. flexsds/server/local_runner.py +208 -0
  25. flexsds/server/modal_app.py +50 -0
  26. flexsds/server/modal_runner.py +98 -0
  27. flexsds/server/vscode_cli.py +82 -0
  28. flexsds-0.2.0.dist-info/METADATA +204 -0
  29. flexsds-0.2.0.dist-info/RECORD +62 -0
  30. flexsds-0.2.0.dist-info/WHEEL +5 -0
  31. flexsds-0.2.0.dist-info/entry_points.txt +2 -0
  32. flexsds-0.2.0.dist-info/top_level.txt +4 -0
  33. pipeline/entity_eval.py +171 -0
  34. pipeline/examples/node1_extractor_input_m0_04_001.txt +9 -0
  35. pipeline/examples/node1_extractor_output_m0_04_001.json +90 -0
  36. pipeline/extractor.py +502 -0
  37. pipeline/invoice_models.py +163 -0
  38. pipeline/node2_questions.py +30 -0
  39. pipeline/openai_compatible.py +87 -0
  40. pipeline/prompts/README.md +18 -0
  41. pipeline/prompts/node1_extractor_system.md +92 -0
  42. pipeline/prompts/node3_report_user.md +5 -0
  43. pipeline/report_writer.py +143 -0
  44. pipeline/schemas/invoice_extraction.schema.json +469 -0
  45. pipeline/sql_guard.py +140 -0
  46. pipeline/sqlite/schema.sql +219 -0
  47. pipeline/text_to_sql.py +170 -0
  48. pipeline/validation.py +166 -0
  49. pipeline/w1_workload.py +139 -0
  50. proposer/__init__.py +31 -0
  51. proposer/adapter.py +153 -0
  52. proposer/controller.py +460 -0
  53. proposer/cost_model.py +216 -0
  54. proposer/tree_adaptive.py +466 -0
  55. server/__init__.py +3 -0
  56. server/adaptive_scheduler.py +215 -0
  57. server/budget_planner.py +287 -0
  58. server/lineage.py +290 -0
  59. server/lineage_diagnostics.py +38 -0
  60. server/lineage_executor.py +129 -0
  61. server/lineage_startup.py +47 -0
  62. server/t4_rowwise_attention.py +262 -0
@@ -0,0 +1,95 @@
1
+ """Live telemetry monitor for inspecting speculative decoding traces in real-time."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import sys
7
+
8
+ try:
9
+ from flexsds.cli.output import print_output as print, write_output
10
+ except ModuleNotFoundError:
11
+ from src.flexsds.cli.output import print_output as print, write_output
12
+ import time
13
+ from typing import Any, Dict, List, Optional
14
+
15
+
16
+ class TelemetryMonitor:
17
+ """Monitors and tails a JSONL trace file containing speculative decoding steps."""
18
+
19
+ def __init__(self, trace_path: str = "telemetry/serving_trace.jsonl") -> None:
20
+ self.trace_path = trace_path
21
+ self._file_pos = 0
22
+
23
+ def read_new_records(self) -> List[Dict[str, Any]]:
24
+ """Reads newly appended records since the last check."""
25
+ if not os.path.exists(self.trace_path):
26
+ return []
27
+
28
+ try:
29
+ with open(self.trace_path, "r", encoding="utf-8") as f:
30
+ f.seek(self._file_pos)
31
+ lines = f.readlines()
32
+ self._file_pos = f.tell()
33
+ except OSError:
34
+ return []
35
+
36
+ records: List[Dict[str, Any]] = []
37
+ for line in lines:
38
+ line_str = line.strip()
39
+ if not line_str:
40
+ continue
41
+ try:
42
+ data = json.loads(line_str)
43
+ if isinstance(data, dict):
44
+ records.append(data)
45
+ except json.JSONDecodeError:
46
+ # Bỏ qua dòng json chưa hoàn chỉnh hoặc hỏng
47
+ continue
48
+ return records
49
+
50
+ @staticmethod
51
+ def format_record(record: Dict[str, Any]) -> str:
52
+ """Formats a single telemetry record into a colorful human-readable row."""
53
+ step = record.get("step", "-")
54
+ lane = record.get("lane", "adaptive")
55
+ executed = record.get("executed_draft_token_count", 0)
56
+ accepted = record.get("accepted_draft_token_count", 0)
57
+ alpha_ema = record.get("alpha_ema", 0.0)
58
+ k = record.get("source_batch_scalar_k", executed)
59
+
60
+ # Tính tỷ lệ chấp nhận trực quan
61
+ ratio_str = f"{alpha_ema:.2f}" if isinstance(alpha_ema, (int, float)) else str(alpha_ema)
62
+ color = "\033[32m" if accepted >= 3 else ("\033[33m" if accepted >= 1 else "\033[31m")
63
+
64
+ return (
65
+ f"[\033[36mStep {step}\033[0m] "
66
+ f"k={k} | "
67
+ f"{color}chấp nhận={accepted}/{executed}\033[0m | "
68
+ f"tỷ lệ={ratio_str} | "
69
+ f"làn={lane}"
70
+ )
71
+
72
+
73
+ def run_live_monitor(
74
+ trace_file: str = "telemetry/serving_trace.jsonl",
75
+ refresh_rate: float = 1.0,
76
+ ) -> int:
77
+ """CLI entry point for streaming telemetry trace updates."""
78
+ monitor = TelemetryMonitor(trace_path=trace_file)
79
+
80
+ print("\n\033[1;37mBộ theo dõi thích ứng thời gian thực (FlexSDS Live Telemetry)\033[0m")
81
+ print(f" Tệp theo dõi: \033[36m{trace_file}\033[0m")
82
+ print(f" Tần suất cập nhật: \033[33m{refresh_rate}s\033[0m | \033[90mNhấn Ctrl+C để dừng\033[0m\n")
83
+
84
+ if not os.path.exists(trace_file):
85
+ print(f" \033[90m(Đang chờ máy chủ tạo tệp trace tại {trace_file}...)\033[0m")
86
+
87
+ try:
88
+ while True:
89
+ records = monitor.read_new_records()
90
+ for r in records:
91
+ print(f" {monitor.format_record(r)}")
92
+ time.sleep(refresh_rate)
93
+ except KeyboardInterrupt:
94
+ print("\n\033[90mĐã dừng bộ theo dõi thích ứng.\033[0m")
95
+ return 0
@@ -0,0 +1,44 @@
1
+ """FlexSDS Core module."""
2
+ try:
3
+ from flexsds.core.tree_topology import (
4
+ TreeTopology,
5
+ CANONICAL_TOPOLOGIES,
6
+ get_tree_topology,
7
+ list_available_topologies,
8
+ )
9
+ from flexsds.core.cost_model import (
10
+ FlexCostModel,
11
+ SolverResult,
12
+ load_profile_for_hardware,
13
+ save_hardware_profile,
14
+ run_hardware_micro_benchmark,
15
+ USER_PROFILES_DIR,
16
+ )
17
+ except ModuleNotFoundError:
18
+ from src.flexsds.core.tree_topology import (
19
+ TreeTopology,
20
+ CANONICAL_TOPOLOGIES,
21
+ get_tree_topology,
22
+ list_available_topologies,
23
+ )
24
+ from src.flexsds.core.cost_model import (
25
+ FlexCostModel,
26
+ SolverResult,
27
+ load_profile_for_hardware,
28
+ save_hardware_profile,
29
+ run_hardware_micro_benchmark,
30
+ USER_PROFILES_DIR,
31
+ )
32
+
33
+ __all__ = [
34
+ "TreeTopology",
35
+ "CANONICAL_TOPOLOGIES",
36
+ "get_tree_topology",
37
+ "list_available_topologies",
38
+ "FlexCostModel",
39
+ "SolverResult",
40
+ "load_profile_for_hardware",
41
+ "save_hardware_profile",
42
+ "run_hardware_micro_benchmark",
43
+ "USER_PROFILES_DIR",
44
+ ]
@@ -0,0 +1,442 @@
1
+ """Compatibility and pre-flight safety guard module for FlexSDS."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import re
7
+ import urllib.request
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Optional
11
+
12
+
13
+ MODEL_CACHE_DIR: Path = Path.home() / ".flexsds" / "cache" / "models"
14
+ EAGLE_ARCHITECTURES: set[str] = {"EagleModel", "EagleForCausalLM", "Eagle3Model"}
15
+
16
+
17
+ class ModelMetadataError(Exception):
18
+ """Ngoại lệ cơ sở cho các lỗi đọc hoặc xử lý metadata của mô hình."""
19
+ pass
20
+
21
+
22
+ class NetworkOfflineError(ModelMetadataError):
23
+ """Ngoại lệ khi không thể kết nối mạng và không tìm thấy cấu hình trong cache."""
24
+ pass
25
+
26
+
27
+ @dataclass
28
+ class PreDownloadCheckResult:
29
+ """Kết quả kiểm tra trước khi tải mô hình từ Hugging Face."""
30
+ is_safe: bool
31
+ vram_needed_mb: float
32
+ free_vram_mb: float
33
+ warning: Optional[str] = None
34
+ suggested_model: Optional[str] = None
35
+ shared_tokenizer: bool = True
36
+
37
+
38
+ @dataclass
39
+ class CompatibilityReport:
40
+ """Pre-flight evaluation report for target and draft models."""
41
+ target_model: str
42
+ draft_model: str
43
+ draft_method: str
44
+ use_heterogeneous_vocab: bool
45
+ target_vram_est_mb: float
46
+ draft_vram_est_mb: float
47
+ kv_cache_min_mb: float
48
+ total_vram_needed_mb: float
49
+ free_vram_mb: float
50
+ oom_risk: bool
51
+ is_compatible: bool
52
+ warnings: List[str] = field(default_factory=list)
53
+ recommendations: List[str] = field(default_factory=list)
54
+
55
+ def summary(self) -> str:
56
+ status = "COMPATIBLE" if self.is_compatible and not self.oom_risk else "WARNING/INCOMPATIBLE"
57
+ return (
58
+ f"Compatibility: {status} | Method: {self.draft_method} | "
59
+ f"VRAM Needed: {self.total_vram_needed_mb / 1024:.1f}GB / Free: {self.free_vram_mb / 1024:.1f}GB | "
60
+ f"Hetero Vocab: {self.use_heterogeneous_vocab}"
61
+ )
62
+
63
+
64
+ def get_model_config(
65
+ model_id_or_path: str,
66
+ cache_dir: Optional[Path] = None,
67
+ timeout_sec: float = 5.0,
68
+ ) -> Dict[str, Any]:
69
+ """Đọc tệp config.json của mô hình từ đường dẫn cục bộ, cache hoặc Hugging Face Hub.
70
+
71
+ Args:
72
+ model_id_or_path: Đường dẫn thư mục/tệp cục bộ hoặc Hugging Face model ID.
73
+ cache_dir: Thư mục lưu cache cục bộ (mặc định ~/.flexsds/cache/models).
74
+ timeout_sec: Thời gian chờ tối đa khi tải qua HTTP (giây).
75
+
76
+ Returns:
77
+ Dict chứa nội dung tệp config.json.
78
+
79
+ Raises:
80
+ NetworkOfflineError: Khi không thể kết nối mạng và chưa có dữ liệu trong cache.
81
+ ModelMetadataError: Khi tệp config.json không tồn tại cục bộ hoặc không hợp lệ.
82
+ """
83
+ # 1. Kiểm tra nếu là đường dẫn cục bộ
84
+ expanded_path = Path(os.path.expanduser(model_id_or_path))
85
+ if expanded_path.exists():
86
+ if expanded_path.is_file() and expanded_path.suffix == ".json":
87
+ config_file = expanded_path
88
+ else:
89
+ config_file = expanded_path / "config.json"
90
+
91
+ if config_file.exists() and config_file.is_file():
92
+ try:
93
+ with open(config_file, "r", encoding="utf-8") as f:
94
+ return json.load(f)
95
+ except Exception as exc:
96
+ raise ModelMetadataError(f"Không thể đọc tệp config cục bộ '{config_file}': {exc}") from exc
97
+ else:
98
+ raise ModelMetadataError(f"Không tìm thấy tệp config.json trong đường dẫn cục bộ '{model_id_or_path}'")
99
+
100
+ # 2. Chuẩn hóa định danh mô hình từ xa và xác định thư mục cache
101
+ clean_id = model_id_or_path.strip().replace("https://huggingface.co/", "").strip("/")
102
+ effective_cache_dir = Path(cache_dir) if cache_dir is not None else MODEL_CACHE_DIR
103
+ model_dir_name = clean_id.replace("/", "--")
104
+ cached_file = effective_cache_dir / model_dir_name / "config.json"
105
+
106
+ # 3. Đọc từ cache cục bộ nếu đã tồn tại
107
+ if cached_file.exists() and cached_file.is_file():
108
+ try:
109
+ with open(cached_file, "r", encoding="utf-8") as f:
110
+ return json.load(f)
111
+ except Exception:
112
+ # Nếu cache bị hỏng, tiếp tục thử tải từ mạng
113
+ pass
114
+
115
+ # 4. Tải từ Hugging Face qua HTTP
116
+ url = f"https://huggingface.co/{clean_id}/raw/main/config.json"
117
+ req = urllib.request.Request(
118
+ url,
119
+ headers={"User-Agent": "FlexSDS-PreFlightGuard/0.1.0"},
120
+ )
121
+ try:
122
+ with urllib.request.urlopen(req, timeout=timeout_sec) as resp:
123
+ raw_bytes = resp.read()
124
+ content = raw_bytes.decode("utf-8") if isinstance(raw_bytes, (bytes, bytearray)) else str(raw_bytes)
125
+ data = json.loads(content)
126
+ except Exception as exc:
127
+ raise NetworkOfflineError(
128
+ f"Không thể tải metadata cho mô hình '{clean_id}' từ Hugging Face Hub: {exc}"
129
+ ) from exc
130
+
131
+ # 5. Lưu vào cache cục bộ cho các lần kiểm tra sau
132
+ try:
133
+ cached_file.parent.mkdir(parents=True, exist_ok=True)
134
+ cached_file.write_text(content, encoding="utf-8")
135
+ except Exception:
136
+ # Lỗi lưu cache không cản trở việc trả về dữ liệu đã tải thành công
137
+ pass
138
+
139
+ return data
140
+
141
+
142
+ def inspect_remote_model_metadata(model_id_or_path: str, timeout_sec: float = 5.0) -> Dict[str, Any]:
143
+ """Đọc tệp config.json (<10 KB) của mô hình từ Hugging Face Hub hoặc thư mục cục bộ."""
144
+ # 1. Kiểm tra nếu là đường dẫn cục bộ
145
+ if os.path.exists(model_id_or_path):
146
+ config_path = (
147
+ model_id_or_path
148
+ if os.path.isfile(model_id_or_path) and model_id_or_path.endswith(".json")
149
+ else os.path.join(model_id_or_path, "config.json")
150
+ )
151
+ if os.path.exists(config_path):
152
+ try:
153
+ with open(config_path, "r", encoding="utf-8") as f:
154
+ return json.load(f)
155
+ except Exception:
156
+ return {}
157
+
158
+ # 2. Truy vấn từ Hugging Face qua HTTP API công khai
159
+ clean_id = model_id_or_path.strip().replace("https://huggingface.co/", "")
160
+ url = f"https://huggingface.co/{clean_id}/raw/main/config.json"
161
+ req = urllib.request.Request(
162
+ url,
163
+ headers={"User-Agent": "FlexSDS-PreFlightGuard/0.1.0"},
164
+ )
165
+ try:
166
+ with urllib.request.urlopen(req, timeout=timeout_sec) as resp:
167
+ content = resp.read().decode("utf-8")
168
+ return json.loads(content)
169
+ except Exception:
170
+ return {}
171
+
172
+
173
+ def pre_download_guard(
174
+ target_model: str,
175
+ draft_model: str,
176
+ free_vram_mb: float,
177
+ mock_metadata: Optional[Dict[str, Any]] = None,
178
+ ) -> PreDownloadCheckResult:
179
+ """Kiểm tra VRAM dự kiến trước khi tải mô hình, ngăn ngừa lãng phí băng thông và OOM."""
180
+ meta = mock_metadata if mock_metadata is not None else inspect_remote_model_metadata(target_model)
181
+ target_lower = target_model.lower()
182
+
183
+ # 1. Xác định số lượng tham số
184
+ num_params: float = 7.0e9
185
+ if "num_parameters" in meta and meta["num_parameters"]:
186
+ num_params = float(meta["num_parameters"])
187
+ else:
188
+ param_match = re.search(r"(\d+(?:\.\d+)?)\s*b", target_lower)
189
+ if param_match:
190
+ num_params = float(param_match.group(1)) * 1e9
191
+
192
+ # 2. Xác định định dạng lượng tử hóa
193
+ quant_cfg = meta.get("quantization_config")
194
+ is_quantized = False
195
+ if quant_cfg is not None:
196
+ is_quantized = True
197
+ elif any(q in target_lower for q in ("awq", "int4", "gptq")):
198
+ is_quantized = True
199
+
200
+ if is_quantized:
201
+ bytes_per_param = 0.55 # AWQ/GPTQ 4-bit + scale/zero overhead
202
+ elif "fp8" in target_lower:
203
+ bytes_per_param = 1.05
204
+ else:
205
+ torch_dtype = str(meta.get("torch_dtype", "")).lower()
206
+ if "float32" in torch_dtype:
207
+ bytes_per_param = 4.0
208
+ else:
209
+ bytes_per_param = 2.0 # FP16 / BF16
210
+
211
+ target_weights_mb = (num_params * bytes_per_param) / (1024.0 * 1024.0)
212
+ draft_weights_mb = estimate_model_vram_mb(draft_model, default_quant="fp16")
213
+ kv_cache_mb = 1200.0 # KV-cache tối thiểu cho serving cơ bản
214
+ total_needed_mb = round(target_weights_mb + draft_weights_mb + kv_cache_mb, 1)
215
+
216
+ is_safe = total_needed_mb <= (free_vram_mb * 0.95)
217
+ warning = None
218
+ suggested_model = None
219
+
220
+ if not is_safe:
221
+ warning = (
222
+ f"Estimated VRAM (~{total_needed_mb / 1024:.1f} GB) exceeds available VRAM "
223
+ f"({free_vram_mb / 1024:.1f} GB free). High risk of OOM!"
224
+ )
225
+ if not is_quantized:
226
+ suggested_model = f"{target_model}-AWQ"
227
+
228
+ # Kiểm tra tokenizer dùng chung
229
+ # Với kiến trúc EAGLE được huấn luyện đồng bộ trên cùng base model, tokenizer là đồng nhất
230
+ shared_tokenizer = True
231
+
232
+ return PreDownloadCheckResult(
233
+ is_safe=is_safe,
234
+ vram_needed_mb=total_needed_mb,
235
+ free_vram_mb=free_vram_mb,
236
+ warning=warning,
237
+ suggested_model=suggested_model,
238
+ shared_tokenizer=shared_tokenizer,
239
+ )
240
+
241
+
242
+
243
+ def estimate_model_vram_mb(model_name: str, default_quant: str = "awq_marlin") -> float:
244
+ """Estimates required VRAM in MB based on model name heuristics."""
245
+ name_lower = model_name.lower()
246
+
247
+ # Speculator Head (EAGLE-3 / EAGLE-3.1 head) is a lightweight 1-layer decoder
248
+ if "eagle" in name_lower or "speculator" in name_lower or "head" in name_lower:
249
+ return 450.0
250
+
251
+ # Extract parameter count (e.g., 7b, 0.5b, 14b, 72b, 8b)
252
+ param_match = re.search(r"(\d+(?:\.\d+)?)\s*b", name_lower)
253
+ params_billion = float(param_match.group(1)) if param_match else 7.0
254
+
255
+ # Determine quantization precision
256
+ if "awq" in name_lower or "int4" in name_lower or "gptq" in name_lower or default_quant == "awq_marlin":
257
+ # 4-bit quantization: ~0.6 GB per billion params + overhead
258
+ return round(params_billion * 600.0 + 500.0, 1)
259
+ elif "fp8" in name_lower:
260
+ # 8-bit quantization: ~1.1 GB per billion params
261
+ return round(params_billion * 1100.0 + 500.0, 1)
262
+ else:
263
+ # Default FP16 / BF16: ~2.1 GB per billion params
264
+ return round(params_billion * 2100.0 + 800.0, 1)
265
+
266
+
267
+ def check_compatibility(
268
+ target_model: str,
269
+ draft_model: str,
270
+ free_vram_mb: float = 23000.0,
271
+ target_quant: str = "fp16",
272
+ max_num_seqs: int = 32,
273
+ max_model_len: int = 4096,
274
+ ) -> CompatibilityReport:
275
+ """Runs pre-flight compatibility and VRAM estimation checks."""
276
+ warnings: List[str] = []
277
+ recommendations: List[str] = []
278
+
279
+ target_lower = target_model.lower()
280
+ draft_lower = draft_model.lower()
281
+
282
+ # 0. Nạp metadata của mô hình từ cache hoặc Hugging Face Hub (dự phòng ngoại lệ khi mất mạng)
283
+ target_cfg: Optional[Dict[str, Any]] = None
284
+ draft_cfg: Optional[Dict[str, Any]] = None
285
+
286
+ try:
287
+ target_cfg = get_model_config(target_model)
288
+ except Exception:
289
+ target_cfg = None
290
+
291
+ try:
292
+ draft_cfg = get_model_config(draft_model)
293
+ except Exception:
294
+ draft_cfg = None
295
+
296
+ # 1. Xác định phương pháp đầu nháp và kiểm tra kiến trúc EAGLE (CR02)
297
+ is_eagle = False
298
+ is_compatible = True
299
+
300
+ if draft_cfg is not None:
301
+ draft_archs = draft_cfg.get("architectures", [])
302
+ draft_mtype = str(draft_cfg.get("model_type", "")).lower()
303
+ if (
304
+ any(a in EAGLE_ARCHITECTURES for a in draft_archs)
305
+ or any("eagle" in str(a).lower() for a in draft_archs)
306
+ or "eagle" in draft_mtype
307
+ ):
308
+ is_eagle = True
309
+ elif not draft_archs and not draft_mtype and ("eagle" in draft_lower or "speculator" in draft_lower):
310
+ is_eagle = True
311
+ else:
312
+ is_eagle = False
313
+ is_compatible = False
314
+ else:
315
+ # Dự phòng khi mất mạng hoặc không tìm thấy metadata: nhận diện theo tên mô hình
316
+ if "eagle" in draft_lower or "speculator" in draft_lower:
317
+ is_eagle = True
318
+ else:
319
+ is_eagle = False
320
+ is_compatible = False
321
+
322
+ draft_method = "eagle3" if is_eagle else "draft_model"
323
+
324
+ # 2. Estimate VRAM Consumption
325
+ target_vram = estimate_model_vram_mb(target_model, default_quant=target_quant)
326
+ draft_vram = estimate_model_vram_mb(draft_model, default_quant="fp16")
327
+
328
+ # KV cache minimum allocation for serving concurrency
329
+ # Rough estimate: 2 * num_layers * hidden_size * kv_heads * seq_len * num_seqs * dtype_size
330
+ kv_cache_min = min(3500.0, max(1500.0, max_num_seqs * max_model_len * 0.02))
331
+
332
+ total_needed = target_vram + draft_vram + kv_cache_min
333
+
334
+ # 3. Check VRAM headroom and OOM risk
335
+ oom_risk = total_needed > (free_vram_mb * 0.95)
336
+ if oom_risk:
337
+ shortfall = total_needed - free_vram_mb
338
+ warnings.append(
339
+ f"VRAM shortfall detected: Required ~{total_needed/1024:.1f} GB, but only {free_vram_mb/1024:.1f} GB free."
340
+ )
341
+ recommendations.append(
342
+ "Use an AWQ/GPTQ 4-bit quantized target model or reduce --max-num-seqs to free KV cache."
343
+ )
344
+
345
+ # 4. Vocabulary Alignment (CR03)
346
+ # So sánh vocab_size giữa target và draft khi có cấu hình; nếu khác nhau thì bật cờ dị thể
347
+ target_vocab = target_cfg.get("vocab_size", 0) if target_cfg else 0
348
+ draft_vocab = draft_cfg.get("vocab_size", 0) if draft_cfg else 0
349
+
350
+ if target_vocab > 0 and draft_vocab > 0:
351
+ use_hetero_vocab = (target_vocab != draft_vocab)
352
+ else:
353
+ target_family = "qwen" if "qwen" in target_lower else ("llama" if "llama" in target_lower else "other")
354
+ draft_family = "qwen" if "qwen" in draft_lower else ("llama" if "llama" in draft_lower else "other")
355
+ use_hetero_vocab = (target_family != draft_family) or is_eagle or ("0.5b" in draft_lower)
356
+
357
+ if use_hetero_vocab:
358
+ recommendations.append(
359
+ "Heterogeneous vocabulary flag enabled (--use-heterogeneous-vocab) for safe draft alignment."
360
+ )
361
+
362
+ # 5. Architecture Family compatibility
363
+ if not is_compatible:
364
+ warnings.append(
365
+ f"Phiên bản FlexSDS hiện tại chỉ hỗ trợ kiến trúc đầu nháp EAGLE. Mô hình nháp '{draft_model}' không được hỗ trợ."
366
+ )
367
+ else:
368
+ target_family = "qwen" if "qwen" in target_lower else ("llama" if "llama" in target_lower else "other")
369
+ draft_family = "qwen" if "qwen" in draft_lower else ("llama" if "llama" in draft_lower else "other")
370
+ if target_family != "other" and draft_family != "other" and target_family != draft_family:
371
+ is_compatible = False
372
+ warnings.append(
373
+ f"Cross-family pairing ({target_family} target with {draft_family} draft) may exhibit low acceptance rate."
374
+ )
375
+
376
+ # 6. Cảnh báo dự phòng ngoại tuyến nếu không nạp được metadata từ mạng hay cache
377
+ if target_cfg is None:
378
+ warnings.append(
379
+ f"Không thể tải metadata cấu hình cho mô hình mục tiêu '{target_model}' (mất mạng hoặc chưa có cache). Sử dụng dự phòng theo tên."
380
+ )
381
+ if draft_cfg is None:
382
+ warnings.append(
383
+ f"Không thể tải metadata cấu hình cho mô hình nháp '{draft_model}' (mất mạng hoặc chưa có cache). Sử dụng dự phòng theo tên."
384
+ )
385
+
386
+ return CompatibilityReport(
387
+ target_model=target_model,
388
+ draft_model=draft_model,
389
+ draft_method=draft_method,
390
+ use_heterogeneous_vocab=use_hetero_vocab,
391
+ target_vram_est_mb=target_vram,
392
+ draft_vram_est_mb=draft_vram,
393
+ kv_cache_min_mb=kv_cache_min,
394
+ total_vram_needed_mb=total_needed,
395
+ free_vram_mb=free_vram_mb,
396
+ oom_risk=oom_risk,
397
+ is_compatible=is_compatible,
398
+ warnings=warnings,
399
+ recommendations=recommendations,
400
+ )
401
+
402
+
403
+ class ServingBlockedError(RuntimeError):
404
+ """A preflight decision prevented any serving action."""
405
+
406
+
407
+ @dataclass
408
+ class ServingPreflight:
409
+ report: CompatibilityReport
410
+ download: PreDownloadCheckResult
411
+ reasons: List[str]
412
+
413
+ @property
414
+ def is_safe(self) -> bool:
415
+ return not self.reasons
416
+
417
+ def require_safe(self) -> None:
418
+ if not self.is_safe:
419
+ raise ServingBlockedError("Launch blocked: " + "; ".join(self.reasons))
420
+
421
+
422
+ def evaluate_serving_preflight(target_model: str, draft_model: str, hw_info,
423
+ backend: str = "local", target_gpu: str = "L4") -> ServingPreflight:
424
+ """One preflight policy shared by flag mode and wizard; no network/weights.
425
+
426
+ Remote GPU budgets are conservative estimates, never local free VRAM or a
427
+ measurement of the eventual container. Runtime memory fit remains unproven.
428
+ """
429
+ budgets = {"T4": 15000.0, "L4": 23000.0, "A10G": 22000.0}
430
+ available = hw_info.free_vram_mb if backend == "local" else budgets.get(target_gpu, 0.0)
431
+ report = check_compatibility(target_model, draft_model, free_vram_mb=available)
432
+ download = pre_download_guard(target_model, draft_model, available, mock_metadata={})
433
+ reasons = []
434
+ if report.oom_risk:
435
+ reasons.append("estimated VRAM exceeds 95% of the available memory budget")
436
+ if not report.is_compatible:
437
+ reasons.append("incompatible target/draft configuration")
438
+ if not download.is_safe:
439
+ reasons.append(download.warning or "pre-download check is unsafe")
440
+ if backend == "local" and not hw_info.is_cuda_available:
441
+ reasons.append("CUDA GPU unavailable; system RAM is not serving VRAM")
442
+ return ServingPreflight(report, download, reasons)