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.
- flexsds/__init__.py +4 -0
- flexsds/assets/ptit_logo.png +0 -0
- flexsds/cli/__init__.py +1 -0
- flexsds/cli/banner.py +84 -0
- flexsds/cli/main.py +330 -0
- flexsds/cli/output.py +69 -0
- flexsds/cli/task_runner.py +171 -0
- flexsds/cli/wizard.py +360 -0
- flexsds/client/__init__.py +4 -0
- flexsds/client/chat.py +137 -0
- flexsds/client/client.py +107 -0
- flexsds/client/monitor.py +95 -0
- flexsds/core/__init__.py +44 -0
- flexsds/core/compatibility.py +442 -0
- flexsds/core/controller.py +502 -0
- flexsds/core/cost_model.py +530 -0
- flexsds/core/credentials.py +111 -0
- flexsds/core/hardware.py +77 -0
- flexsds/core/tree_topology.py +296 -0
- flexsds/profiles/profile_sm75_t4.json +62 -0
- flexsds/profiles/profile_sm89_l4.json +194 -0
- flexsds/server/__init__.py +1 -0
- flexsds/server/colab_runner.py +62 -0
- flexsds/server/local_runner.py +208 -0
- flexsds/server/modal_app.py +50 -0
- flexsds/server/modal_runner.py +98 -0
- flexsds/server/vscode_cli.py +82 -0
- flexsds-0.2.0.dist-info/METADATA +204 -0
- flexsds-0.2.0.dist-info/RECORD +62 -0
- flexsds-0.2.0.dist-info/WHEEL +5 -0
- flexsds-0.2.0.dist-info/entry_points.txt +2 -0
- flexsds-0.2.0.dist-info/top_level.txt +4 -0
- pipeline/entity_eval.py +171 -0
- pipeline/examples/node1_extractor_input_m0_04_001.txt +9 -0
- pipeline/examples/node1_extractor_output_m0_04_001.json +90 -0
- pipeline/extractor.py +502 -0
- pipeline/invoice_models.py +163 -0
- pipeline/node2_questions.py +30 -0
- pipeline/openai_compatible.py +87 -0
- pipeline/prompts/README.md +18 -0
- pipeline/prompts/node1_extractor_system.md +92 -0
- pipeline/prompts/node3_report_user.md +5 -0
- pipeline/report_writer.py +143 -0
- pipeline/schemas/invoice_extraction.schema.json +469 -0
- pipeline/sql_guard.py +140 -0
- pipeline/sqlite/schema.sql +219 -0
- pipeline/text_to_sql.py +170 -0
- pipeline/validation.py +166 -0
- pipeline/w1_workload.py +139 -0
- proposer/__init__.py +31 -0
- proposer/adapter.py +153 -0
- proposer/controller.py +460 -0
- proposer/cost_model.py +216 -0
- proposer/tree_adaptive.py +466 -0
- server/__init__.py +3 -0
- server/adaptive_scheduler.py +215 -0
- server/budget_planner.py +287 -0
- server/lineage.py +290 -0
- server/lineage_diagnostics.py +38 -0
- server/lineage_executor.py +129 -0
- server/lineage_startup.py +47 -0
- 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
|
flexsds/core/__init__.py
ADDED
|
@@ -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)
|