@tuned-tensor/cli 0.11.0 → 0.12.0-beta.1
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.
- package/README.md +44 -29
- package/dist/{chunk-DAP2Z7EZ.js → chunk-HDGBDHOB.js} +474 -74
- package/dist/chunk-HDGBDHOB.js.map +1 -0
- package/dist/index.js +366 -199
- package/dist/index.js.map +1 -1
- package/dist/local-runtime.js +52 -19
- package/dist/local-runtime.js.map +1 -1
- package/docs/local-runtime/architecture.md +8 -2
- package/docs/local-runtime/spark.md +39 -2
- package/package.json +6 -2
- package/training/local-runner/pyproject.toml +2 -2
- package/training/local-runner/src/evaluate.py +44 -18
- package/training/local-runner/src/model_contract.py +134 -14
- package/training/local-runner/src/prefetch.py +25 -8
- package/training/local-runner/src/serve.py +14 -6
- package/training/local-runner/src/sft_data.py +7 -3
- package/training/local-runner/src/train.py +48 -14
- package/training/local-runner/uv.lock +54 -54
- package/dist/chunk-DAP2Z7EZ.js.map +0 -1
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
> `tt-local` is deprecated. Install `@tuned-tensor/cli` and use `tt local`.
|
|
5
5
|
> Existing projects still work.
|
|
6
6
|
|
|
7
|
-
DGX Spark is the reference host for
|
|
8
|
-
`Qwen/Qwen3.5-2B
|
|
7
|
+
DGX Spark is the reference host for local text SFT with LoRA adapters. TT Local
|
|
8
|
+
certifies `Qwen/Qwen3.5-2B`, the larger
|
|
9
|
+
`nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16` checkpoint, and
|
|
10
|
+
`meta-models/Muse-Glimmer-30B`.
|
|
9
11
|
|
|
10
12
|
## Check the host
|
|
11
13
|
|
|
@@ -28,6 +30,41 @@ cd ~/tuned-tensor-runs/support-adapter
|
|
|
28
30
|
tt local init --name "Support Adapter" --model Qwen/Qwen3.5-2B --profile spark
|
|
29
31
|
```
|
|
30
32
|
|
|
33
|
+
For a Nemotron execution worker:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
mkdir -p ~/tuned-tensor-runs/nemotron-worker
|
|
37
|
+
cd ~/tuned-tensor-runs/nemotron-worker
|
|
38
|
+
tt local init \
|
|
39
|
+
--name "Nemotron Worker" \
|
|
40
|
+
--model nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16 \
|
|
41
|
+
--profile spark
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Nemotron uses the BF16 customization checkpoint rather than the NVFP4
|
|
45
|
+
deployment checkpoint. Its defaults enable activation checkpointing, cap the
|
|
46
|
+
sequence length at 1,024 tokens, and apply LoRA only to shared attention and
|
|
47
|
+
Mamba projections. The base weights require about 66 GB before cache and
|
|
48
|
+
training overhead, so this path is not intended for ordinary consumer GPUs.
|
|
49
|
+
|
|
50
|
+
For a Muse Glimmer execution worker:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
mkdir -p ~/tuned-tensor-runs/muse-glimmer-worker
|
|
54
|
+
cd ~/tuned-tensor-runs/muse-glimmer-worker
|
|
55
|
+
tt local init \
|
|
56
|
+
--name "Muse Glimmer Worker" \
|
|
57
|
+
--model meta-models/Muse-Glimmer-30B \
|
|
58
|
+
--profile spark
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Muse Glimmer is a vision-language checkpoint. The runtime fine-tunes its text
|
|
62
|
+
tower only: it loads the checkpoint through `AutoModelForImageTextToText` (the
|
|
63
|
+
text tower is not exposed in the causal-LM auto mapping) and leaves the vision
|
|
64
|
+
tower frozen and unused. Defaults enable activation checkpointing and cap the
|
|
65
|
+
sequence length at 2,048 tokens; the base weights are comparable to Nemotron
|
|
66
|
+
and require Spark-class unified memory.
|
|
67
|
+
|
|
31
68
|
Edit both generated examples in `tunedtensor.json`. For a meaningful run,
|
|
32
69
|
replace them with a larger, representative dataset and a separate validation
|
|
33
70
|
split.
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuned-tensor/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Local
|
|
3
|
+
"version": "0.12.0-beta.1",
|
|
4
|
+
"description": "Local TT agent and unified cloud/local CLI for Tuned Tensor workflows",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/tunedtensor/tuned-tensor-cli.git"
|
|
8
|
+
},
|
|
5
9
|
"type": "module",
|
|
6
10
|
"bin": {
|
|
7
11
|
"tt": "dist/index.js"
|
|
@@ -5,10 +5,10 @@ requires-python = ">=3.11"
|
|
|
5
5
|
dependencies = [
|
|
6
6
|
"accelerate>=1.0.0",
|
|
7
7
|
"huggingface-hub>=0.36.0",
|
|
8
|
-
"peft>=0.
|
|
8
|
+
"peft>=0.20.0",
|
|
9
9
|
"safetensors>=0.4.5",
|
|
10
10
|
"torch>=2.11.0",
|
|
11
|
-
"transformers>=
|
|
11
|
+
"transformers>=5.15.0",
|
|
12
12
|
]
|
|
13
13
|
|
|
14
14
|
[tool.uv.sources]
|
|
@@ -10,8 +10,12 @@ from tempfile import TemporaryDirectory
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
12
|
from model_contract import (
|
|
13
|
+
CERTIFIED_BASE_MODELS,
|
|
13
14
|
CERTIFIED_BASE_MODEL,
|
|
15
|
+
assert_certified_base_model,
|
|
16
|
+
assert_certified_base_model_revision,
|
|
14
17
|
assert_certified_model_config,
|
|
18
|
+
chat_template_kwargs,
|
|
15
19
|
)
|
|
16
20
|
|
|
17
21
|
MAX_ARCHIVE_MEMBERS = 20_000
|
|
@@ -46,18 +50,20 @@ def configure_hugging_face_cache(cache_home: str | None) -> None:
|
|
|
46
50
|
|
|
47
51
|
def import_runtime_dependencies() -> None:
|
|
48
52
|
"""Import libraries only after Hugging Face cache settings are final."""
|
|
49
|
-
global torch, PeftModel, AutoModelForCausalLM, AutoTokenizer
|
|
53
|
+
global torch, PeftModel, AutoModelForCausalLM, AutoModelForImageTextToText, AutoTokenizer
|
|
50
54
|
|
|
51
55
|
import torch as torch_module
|
|
52
56
|
from peft import PeftModel as peft_model
|
|
53
57
|
from transformers import (
|
|
54
58
|
AutoModelForCausalLM as auto_model_for_causal_lm,
|
|
59
|
+
AutoModelForImageTextToText as auto_model_for_image_text_to_text,
|
|
55
60
|
AutoTokenizer as auto_tokenizer,
|
|
56
61
|
)
|
|
57
62
|
|
|
58
63
|
torch = torch_module
|
|
59
64
|
PeftModel = peft_model
|
|
60
65
|
AutoModelForCausalLM = auto_model_for_causal_lm
|
|
66
|
+
AutoModelForImageTextToText = auto_model_for_image_text_to_text
|
|
61
67
|
AutoTokenizer = auto_tokenizer
|
|
62
68
|
|
|
63
69
|
|
|
@@ -111,6 +117,7 @@ def format_prompt(
|
|
|
111
117
|
tokenizer: Any,
|
|
112
118
|
system: str,
|
|
113
119
|
prompt: str,
|
|
120
|
+
base_model: str = CERTIFIED_BASE_MODEL,
|
|
114
121
|
) -> str:
|
|
115
122
|
messages = [
|
|
116
123
|
{"role": "system", "content": system},
|
|
@@ -121,9 +128,10 @@ def format_prompt(
|
|
|
121
128
|
messages,
|
|
122
129
|
tokenize=False,
|
|
123
130
|
add_generation_prompt=True,
|
|
131
|
+
**chat_template_kwargs(base_model),
|
|
124
132
|
)
|
|
125
133
|
except Exception as exc:
|
|
126
|
-
raise ValueError(f"
|
|
134
|
+
raise ValueError(f"Model chat-template rendering failed: {exc}") from exc
|
|
127
135
|
|
|
128
136
|
|
|
129
137
|
def resolve_device(device: str) -> str:
|
|
@@ -135,30 +143,37 @@ def resolve_device(device: str) -> str:
|
|
|
135
143
|
|
|
136
144
|
|
|
137
145
|
def _assert_certified_model_source(base_model: str) -> None:
|
|
138
|
-
if not Path(base_model).exists() and base_model
|
|
146
|
+
if not Path(base_model).exists() and base_model not in CERTIFIED_BASE_MODELS:
|
|
139
147
|
raise ValueError(
|
|
140
|
-
f"The bundled evaluator
|
|
148
|
+
f"The bundled evaluator does not certify {base_model!r}"
|
|
141
149
|
)
|
|
142
150
|
|
|
143
151
|
|
|
144
|
-
def load_text_model(
|
|
152
|
+
def load_text_model(
|
|
153
|
+
payload: dict[str, Any],
|
|
154
|
+
adapter_path: str | None,
|
|
155
|
+
model_loader: str = "causal_lm",
|
|
156
|
+
):
|
|
145
157
|
base_model = str(payload["base_model"])
|
|
146
|
-
|
|
158
|
+
model_source = str(payload.get("model_source", base_model))
|
|
159
|
+
assert_certified_base_model(base_model, "Evaluation base model")
|
|
160
|
+
_assert_certified_model_source(model_source)
|
|
161
|
+
revision = payload.get("base_model_revision")
|
|
162
|
+
assert_certified_base_model_revision(base_model, revision, "Evaluation base model revision")
|
|
147
163
|
device = resolve_device(str(payload.get("device", "cuda")))
|
|
148
164
|
token = os.getenv("HF_TOKEN")
|
|
149
|
-
revision = payload.get("base_model_revision")
|
|
150
165
|
source_kwargs: dict[str, Any] = {
|
|
151
166
|
"trust_remote_code": False,
|
|
152
167
|
"token": token,
|
|
153
168
|
}
|
|
154
|
-
if not Path(
|
|
169
|
+
if not Path(model_source).exists():
|
|
155
170
|
source_kwargs["local_files_only"] = True
|
|
156
|
-
if revision and not Path(
|
|
171
|
+
if revision and not Path(model_source).exists():
|
|
157
172
|
source_kwargs["revision"] = revision
|
|
158
173
|
|
|
159
|
-
tokenizer = AutoTokenizer.from_pretrained(
|
|
174
|
+
tokenizer = AutoTokenizer.from_pretrained(model_source, **source_kwargs)
|
|
160
175
|
if tokenizer.eos_token_id is None:
|
|
161
|
-
raise ValueError(f"{
|
|
176
|
+
raise ValueError(f"{base_model} tokenizer has no EOS token")
|
|
162
177
|
if tokenizer.pad_token_id is None:
|
|
163
178
|
tokenizer.pad_token = tokenizer.eos_token
|
|
164
179
|
|
|
@@ -172,8 +187,13 @@ def load_text_model(payload: dict[str, Any], adapter_path: str | None):
|
|
|
172
187
|
if device == "cuda":
|
|
173
188
|
model_kwargs["device_map"] = {"": torch.cuda.current_device()}
|
|
174
189
|
|
|
175
|
-
|
|
176
|
-
|
|
190
|
+
model_class = (
|
|
191
|
+
AutoModelForImageTextToText
|
|
192
|
+
if model_loader == "image_text_to_text"
|
|
193
|
+
else AutoModelForCausalLM
|
|
194
|
+
)
|
|
195
|
+
model = model_class.from_pretrained(model_source, **model_kwargs)
|
|
196
|
+
assert_certified_model_config(model.config, expected_model_id=base_model)
|
|
177
197
|
if adapter_path:
|
|
178
198
|
model = PeftModel.from_pretrained(model, adapter_path)
|
|
179
199
|
if device != "cuda":
|
|
@@ -199,11 +219,12 @@ def generate_text_one(
|
|
|
199
219
|
system: str,
|
|
200
220
|
example: dict[str, Any],
|
|
201
221
|
generation: dict[str, Any],
|
|
222
|
+
base_model: str = CERTIFIED_BASE_MODEL,
|
|
202
223
|
) -> dict[str, Any]:
|
|
203
224
|
if example.get("input_assets"):
|
|
204
|
-
raise ValueError("The bundled
|
|
225
|
+
raise ValueError("The bundled SFT evaluator is text-only and does not accept input_assets")
|
|
205
226
|
prompt = str(example["input"])
|
|
206
|
-
formatted = format_prompt(tokenizer, system, prompt)
|
|
227
|
+
formatted = format_prompt(tokenizer, system, prompt, base_model)
|
|
207
228
|
inputs = tokenizer(formatted, return_tensors="pt")
|
|
208
229
|
target_device = next(model.parameters()).device
|
|
209
230
|
inputs = {key: value.to(target_device) for key, value in inputs.items()}
|
|
@@ -235,8 +256,12 @@ def main() -> None:
|
|
|
235
256
|
payload = load_json(Path(args.input))
|
|
236
257
|
if payload.get("protocol_version") != 2:
|
|
237
258
|
raise ValueError("Unsupported inference protocol; expected protocol_version 2")
|
|
238
|
-
|
|
239
|
-
|
|
259
|
+
model_loader = str(payload.get("model_loader") or "causal_lm")
|
|
260
|
+
if model_loader not in ("causal_lm", "image_text_to_text"):
|
|
261
|
+
raise ValueError(
|
|
262
|
+
f"The bundled evaluator does not support model_loader={model_loader!r}; "
|
|
263
|
+
"expected causal_lm or image_text_to_text"
|
|
264
|
+
)
|
|
240
265
|
configure_hugging_face_cache(payload.get("model_cache"))
|
|
241
266
|
import_runtime_dependencies()
|
|
242
267
|
|
|
@@ -245,7 +270,7 @@ def main() -> None:
|
|
|
245
270
|
generation = payload.get("generation", {})
|
|
246
271
|
if not isinstance(generation, dict):
|
|
247
272
|
raise ValueError("generation must be a JSON object")
|
|
248
|
-
model, tokenizer, _device = load_text_model(payload, adapter_path)
|
|
273
|
+
model, tokenizer, _device = load_text_model(payload, adapter_path, model_loader)
|
|
249
274
|
results = [
|
|
250
275
|
generate_text_one(
|
|
251
276
|
model,
|
|
@@ -253,6 +278,7 @@ def main() -> None:
|
|
|
253
278
|
str(payload.get("system", "")),
|
|
254
279
|
example,
|
|
255
280
|
generation,
|
|
281
|
+
str(payload["base_model"]),
|
|
256
282
|
)
|
|
257
283
|
for example in payload.get("examples", [])
|
|
258
284
|
]
|
|
@@ -4,7 +4,16 @@ from typing import Any
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
CERTIFIED_BASE_MODEL = "Qwen/Qwen3.5-2B"
|
|
7
|
-
|
|
7
|
+
NEMOTRON_BASE_MODEL = "nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16"
|
|
8
|
+
MUSE_GLIMMER_BASE_MODEL = "meta-models/Muse-Glimmer-30B"
|
|
9
|
+
# Immutable Hugging Face revision reviewed for Nemotron local fine-tuning.
|
|
10
|
+
# Node validates local snapshots against this revision and its certified file
|
|
11
|
+
# digests before invoking Python.
|
|
12
|
+
NEMOTRON_BASE_MODEL_REVISION = "ce38b6ab8b252b4b8ee7165b4605e93191cafd73"
|
|
13
|
+
# Immutable Hugging Face revision reviewed and certified for Muse Glimmer.
|
|
14
|
+
MUSE_GLIMMER_BASE_MODEL_REVISION = "a4e59da52a7bc87ae7251dd5545c0dd437c44b68"
|
|
15
|
+
CERTIFIED_BASE_MODELS = (CERTIFIED_BASE_MODEL, NEMOTRON_BASE_MODEL, MUSE_GLIMMER_BASE_MODEL)
|
|
16
|
+
CERTIFIED_QWEN_TEXT_CONFIG = {
|
|
8
17
|
"model_type": "qwen3_5_text",
|
|
9
18
|
"hidden_size": 2048,
|
|
10
19
|
"num_hidden_layers": 24,
|
|
@@ -13,6 +22,27 @@ CERTIFIED_TEXT_CONFIG = {
|
|
|
13
22
|
"intermediate_size": 6144,
|
|
14
23
|
"vocab_size": 248320,
|
|
15
24
|
}
|
|
25
|
+
CERTIFIED_MUSE_GLIMMER_TEXT_CONFIG = {
|
|
26
|
+
"model_type": "muse_glimmer_text",
|
|
27
|
+
"hidden_size": 6656,
|
|
28
|
+
"num_hidden_layers": 52,
|
|
29
|
+
"num_attention_heads": 32,
|
|
30
|
+
"num_key_value_heads": 2,
|
|
31
|
+
"intermediate_size": 19968,
|
|
32
|
+
"vocab_size": 202048,
|
|
33
|
+
}
|
|
34
|
+
CERTIFIED_NEMOTRON_CONFIG = {
|
|
35
|
+
"hidden_size": 2688,
|
|
36
|
+
"num_hidden_layers": 52,
|
|
37
|
+
"num_attention_heads": 32,
|
|
38
|
+
"num_key_value_heads": 2,
|
|
39
|
+
"intermediate_size": 1856,
|
|
40
|
+
"vocab_size": 131072,
|
|
41
|
+
"n_routed_experts": 128,
|
|
42
|
+
"num_experts_per_tok": 6,
|
|
43
|
+
"num_nextn_predict_layers": 1,
|
|
44
|
+
"max_position_embeddings": 262144,
|
|
45
|
+
}
|
|
16
46
|
|
|
17
47
|
|
|
18
48
|
def _field(value: Any, name: str) -> Any:
|
|
@@ -21,27 +51,117 @@ def _field(value: Any, name: str) -> Any:
|
|
|
21
51
|
return getattr(value, name, None)
|
|
22
52
|
|
|
23
53
|
|
|
24
|
-
def assert_certified_model_config(
|
|
54
|
+
def assert_certified_model_config(
|
|
55
|
+
value: Any,
|
|
56
|
+
label: str = "base-model config",
|
|
57
|
+
expected_model_id: str | None = None,
|
|
58
|
+
) -> None:
|
|
25
59
|
model_type = _field(value, "model_type")
|
|
26
60
|
architectures = _field(value, "architectures")
|
|
27
|
-
|
|
28
|
-
|
|
61
|
+
|
|
62
|
+
if model_type in ("qwen3_5", "qwen3_5_text"):
|
|
63
|
+
actual_model_id = CERTIFIED_BASE_MODEL
|
|
64
|
+
if model_type == "qwen3_5":
|
|
65
|
+
text_config = _field(value, "text_config")
|
|
66
|
+
if (
|
|
67
|
+
not isinstance(architectures, (list, tuple))
|
|
68
|
+
or "Qwen3_5ForConditionalGeneration" not in architectures
|
|
69
|
+
or text_config is None
|
|
70
|
+
):
|
|
71
|
+
raise ValueError(f"{label} is not the certified {CERTIFIED_BASE_MODEL} architecture")
|
|
72
|
+
else:
|
|
73
|
+
# AutoModelForCausalLM exposes the selected text sub-config after it
|
|
74
|
+
# dispatches the verified repository-level Qwen3.5 config.
|
|
75
|
+
text_config = value
|
|
76
|
+
certified = CERTIFIED_QWEN_TEXT_CONFIG
|
|
77
|
+
elif model_type in ("muse_glimmer", "muse_glimmer_text"):
|
|
78
|
+
actual_model_id = MUSE_GLIMMER_BASE_MODEL
|
|
79
|
+
if model_type == "muse_glimmer":
|
|
80
|
+
text_config = _field(value, "text_config")
|
|
81
|
+
if (
|
|
82
|
+
not isinstance(architectures, (list, tuple))
|
|
83
|
+
or "MuseGlimmerForConditionalGeneration" not in architectures
|
|
84
|
+
or text_config is None
|
|
85
|
+
):
|
|
86
|
+
raise ValueError(f"{label} is not the certified {MUSE_GLIMMER_BASE_MODEL} architecture")
|
|
87
|
+
else:
|
|
88
|
+
text_config = value
|
|
89
|
+
certified = CERTIFIED_MUSE_GLIMMER_TEXT_CONFIG
|
|
90
|
+
elif model_type == "nemotron_h":
|
|
91
|
+
actual_model_id = NEMOTRON_BASE_MODEL
|
|
29
92
|
if (
|
|
30
93
|
not isinstance(architectures, (list, tuple))
|
|
31
|
-
or "
|
|
32
|
-
or text_config is None
|
|
94
|
+
or "NemotronHForCausalLM" not in architectures
|
|
33
95
|
):
|
|
34
|
-
raise ValueError(f"{label} is not the certified {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
96
|
+
raise ValueError(f"{label} is not the certified {NEMOTRON_BASE_MODEL} architecture")
|
|
97
|
+
for name, expected in CERTIFIED_NEMOTRON_CONFIG.items():
|
|
98
|
+
actual = _field(value, name)
|
|
99
|
+
if actual != expected:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"{label} is not the certified {NEMOTRON_BASE_MODEL} architecture: "
|
|
102
|
+
f"{name} must be {expected!r}, got {actual!r}"
|
|
103
|
+
)
|
|
104
|
+
if expected_model_id and expected_model_id != actual_model_id:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"{label} does not match requested base model {expected_model_id}; "
|
|
107
|
+
f"it matches {actual_model_id}"
|
|
108
|
+
)
|
|
109
|
+
return
|
|
39
110
|
else:
|
|
40
|
-
raise ValueError(f"{label} is not
|
|
41
|
-
|
|
111
|
+
raise ValueError(f"{label} is not a certified TT Local base-model architecture")
|
|
112
|
+
|
|
113
|
+
for name, expected in certified.items():
|
|
42
114
|
actual = _field(text_config, name)
|
|
43
115
|
if actual != expected:
|
|
44
116
|
raise ValueError(
|
|
45
|
-
f"{label} is not the certified {
|
|
117
|
+
f"{label} is not the certified {actual_model_id} architecture: "
|
|
46
118
|
f"text_config.{name} must be {expected!r}, got {actual!r}"
|
|
47
119
|
)
|
|
120
|
+
if expected_model_id and expected_model_id != actual_model_id:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"{label} does not match requested base model {expected_model_id}; "
|
|
123
|
+
f"it matches {actual_model_id}"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def assert_certified_base_model(model_id: str, label: str = "base model") -> None:
|
|
128
|
+
if model_id not in CERTIFIED_BASE_MODELS:
|
|
129
|
+
supported = ", ".join(CERTIFIED_BASE_MODELS)
|
|
130
|
+
raise ValueError(f"{label} must be one of {supported}; got {model_id!r}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def assert_certified_base_model_revision(base_model: str, revision: str | None, label: str = "base model revision") -> None:
|
|
134
|
+
"""Require the certified immutable revision for pinned training models.
|
|
135
|
+
|
|
136
|
+
Qwen remains unpinned for backward compatibility. Nemotron and Muse
|
|
137
|
+
Glimmer loads are bound here; local snapshot contents are additionally
|
|
138
|
+
verified by the Node runtime.
|
|
139
|
+
"""
|
|
140
|
+
expected = {
|
|
141
|
+
NEMOTRON_BASE_MODEL: NEMOTRON_BASE_MODEL_REVISION,
|
|
142
|
+
MUSE_GLIMMER_BASE_MODEL: MUSE_GLIMMER_BASE_MODEL_REVISION,
|
|
143
|
+
}.get(base_model)
|
|
144
|
+
if expected is None:
|
|
145
|
+
return
|
|
146
|
+
if revision is None:
|
|
147
|
+
raise ValueError(f"{label} is required for {base_model}")
|
|
148
|
+
if revision != expected:
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f"{label} must be {expected} for {base_model}; got {revision!r}"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def parse_lora_target_modules(value: str | None) -> str | list[str]:
|
|
155
|
+
raw = (value or "all-linear").strip()
|
|
156
|
+
if raw == "all-linear":
|
|
157
|
+
return raw
|
|
158
|
+
modules = [item.strip() for item in raw.split(",")]
|
|
159
|
+
if not modules or any(not item for item in modules):
|
|
160
|
+
raise ValueError("LoRA target modules must be 'all-linear' or a comma-separated non-empty list")
|
|
161
|
+
return modules
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def chat_template_kwargs(model_id: str) -> dict[str, Any]:
|
|
165
|
+
if model_id == NEMOTRON_BASE_MODEL:
|
|
166
|
+
return {"enable_thinking": False}
|
|
167
|
+
return {}
|
|
@@ -7,11 +7,17 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
from model_contract import (
|
|
10
|
-
|
|
10
|
+
assert_certified_base_model,
|
|
11
|
+
assert_certified_base_model_revision,
|
|
11
12
|
assert_certified_model_config,
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
ALLOW_PATTERNS = [
|
|
16
|
+
".gitattributes",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"README.md",
|
|
19
|
+
"*.md",
|
|
20
|
+
"*.png",
|
|
15
21
|
"*.json",
|
|
16
22
|
"*.model",
|
|
17
23
|
"*.safetensors",
|
|
@@ -79,13 +85,17 @@ def hash_file(path: Path, algorithm: str, prefix: bytes = b"") -> str:
|
|
|
79
85
|
return digest.hexdigest()
|
|
80
86
|
|
|
81
87
|
|
|
82
|
-
def verify_snapshot(snapshot: Path) -> tuple[list[Path], int]:
|
|
88
|
+
def verify_snapshot(snapshot: Path, expected_model_id: str | None = None) -> tuple[list[Path], int]:
|
|
83
89
|
config = snapshot / "config.json"
|
|
84
90
|
if not config.is_file() or config.stat().st_size == 0:
|
|
85
91
|
raise ValueError(f"Cached snapshot is missing a non-empty config.json: {snapshot}")
|
|
86
92
|
try:
|
|
87
93
|
parsed_config = json.loads(config.read_text(encoding="utf-8"))
|
|
88
|
-
assert_certified_model_config(
|
|
94
|
+
assert_certified_model_config(
|
|
95
|
+
parsed_config,
|
|
96
|
+
f"Cached config {config}",
|
|
97
|
+
expected_model_id=expected_model_id,
|
|
98
|
+
)
|
|
89
99
|
except (OSError, json.JSONDecodeError) as exc:
|
|
90
100
|
raise ValueError(f"Cached snapshot has an invalid config.json: {snapshot}") from exc
|
|
91
101
|
|
|
@@ -144,10 +154,13 @@ def main() -> None:
|
|
|
144
154
|
|
|
145
155
|
payload = json.loads(Path(args.input).read_text(encoding="utf-8"))
|
|
146
156
|
base_model = str(payload["base_model"])
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
157
|
+
assert_certified_base_model(base_model, "Prefetch base model")
|
|
158
|
+
requested_revision = payload.get("revision")
|
|
159
|
+
assert_certified_base_model_revision(
|
|
160
|
+
base_model,
|
|
161
|
+
requested_revision,
|
|
162
|
+
"Prefetch base model revision",
|
|
163
|
+
)
|
|
151
164
|
cache_dir = payload.get("model_cache")
|
|
152
165
|
configure_hugging_face_cache(cache_dir)
|
|
153
166
|
|
|
@@ -171,8 +184,12 @@ def main() -> None:
|
|
|
171
184
|
raise ValueError(
|
|
172
185
|
"Hugging Face snapshot did not resolve to a 40-character immutable commit SHA"
|
|
173
186
|
)
|
|
187
|
+
if requested_revision and snapshot_revision.lower() != str(requested_revision).lower():
|
|
188
|
+
raise ValueError(
|
|
189
|
+
f"Hugging Face returned revision {snapshot_revision}, not requested revision {requested_revision}"
|
|
190
|
+
)
|
|
174
191
|
print(f"Verifying snapshot files under {snapshot}...", flush=True)
|
|
175
|
-
snapshot_files, verified_blob_count = verify_snapshot(snapshot)
|
|
192
|
+
snapshot_files, verified_blob_count = verify_snapshot(snapshot, base_model)
|
|
176
193
|
|
|
177
194
|
write_json(args.output, {
|
|
178
195
|
"ok": True,
|
|
@@ -20,10 +20,12 @@ from evaluate import (
|
|
|
20
20
|
resolve_adapter_path,
|
|
21
21
|
sampling_kwargs,
|
|
22
22
|
)
|
|
23
|
+
from model_contract import assert_certified_base_model_revision, chat_template_kwargs
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
MODEL_ARTIFACT = os.environ.get("TT_MODEL_ARTIFACT")
|
|
26
27
|
BASE_MODEL = os.environ["TT_BASE_MODEL"]
|
|
28
|
+
MODEL_SOURCE = os.environ.get("TT_MODEL_SOURCE", BASE_MODEL)
|
|
27
29
|
BASE_MODEL_REVISION = os.environ.get("TT_BASE_MODEL_REVISION")
|
|
28
30
|
MODEL_NAME = os.environ.get("TT_MODEL_NAME", "tuned-tensor-local")
|
|
29
31
|
MODEL_LOADER = os.environ.get("TT_MODEL_LOADER", "causal_lm")
|
|
@@ -42,8 +44,12 @@ API_KEY = os.environ.get("TT_API_KEY", "")
|
|
|
42
44
|
MAX_CONCURRENT_REQUESTS = int(os.environ.get("TT_MAX_CONCURRENT_REQUESTS", "1"))
|
|
43
45
|
|
|
44
46
|
|
|
45
|
-
if MODEL_LOADER
|
|
46
|
-
raise ValueError(
|
|
47
|
+
if MODEL_LOADER not in ("causal_lm", "image_text_to_text"):
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"The bundled model server does not support TT_MODEL_LOADER={MODEL_LOADER!r}; "
|
|
50
|
+
"expected causal_lm or image_text_to_text"
|
|
51
|
+
)
|
|
52
|
+
assert_certified_base_model_revision(BASE_MODEL, BASE_MODEL_REVISION, "Serving base model revision")
|
|
47
53
|
configure_hugging_face_cache(os.environ.get("HF_HOME"))
|
|
48
54
|
import_runtime_dependencies()
|
|
49
55
|
TEMP_DIR = TemporaryDirectory(prefix="tt-local-serve-")
|
|
@@ -54,11 +60,12 @@ ADAPTER_PATH = (
|
|
|
54
60
|
)
|
|
55
61
|
MODEL_PAYLOAD = {
|
|
56
62
|
"base_model": BASE_MODEL,
|
|
63
|
+
"model_source": MODEL_SOURCE,
|
|
57
64
|
"base_model_revision": BASE_MODEL_REVISION,
|
|
58
65
|
"device": DEVICE_REQUEST,
|
|
59
66
|
"model_loader": MODEL_LOADER,
|
|
60
67
|
}
|
|
61
|
-
MODEL, TOKENIZER, DEVICE = load_text_model(MODEL_PAYLOAD, ADAPTER_PATH)
|
|
68
|
+
MODEL, TOKENIZER, DEVICE = load_text_model(MODEL_PAYLOAD, ADAPTER_PATH, MODEL_LOADER)
|
|
62
69
|
GENERATION_LOCK = threading.Lock()
|
|
63
70
|
REQUEST_SLOTS = threading.BoundedSemaphore(MAX_CONCURRENT_REQUESTS)
|
|
64
71
|
|
|
@@ -73,7 +80,7 @@ def text_content(content: Any) -> str:
|
|
|
73
80
|
parts: list[str] = []
|
|
74
81
|
for part in content:
|
|
75
82
|
if not isinstance(part, dict) or part.get("type") != "text":
|
|
76
|
-
raise ValueError("The bundled
|
|
83
|
+
raise ValueError("The bundled model server accepts text content only.")
|
|
77
84
|
text = part.get("text")
|
|
78
85
|
if not isinstance(text, str):
|
|
79
86
|
raise ValueError("Every text content part must contain a string text field.")
|
|
@@ -104,7 +111,7 @@ def normalize_messages(raw_messages: Any) -> list[dict[str, str]]:
|
|
|
104
111
|
raise ValueError("Request must contain at least one non-system message.")
|
|
105
112
|
messages: list[dict[str, str]] = []
|
|
106
113
|
if system_parts:
|
|
107
|
-
#
|
|
114
|
+
# The certified chat templates accept one leading system message. Merge the invariant owner
|
|
108
115
|
# prompt and any client context instead of triggering an implicit
|
|
109
116
|
# template fallback with duplicate system turns.
|
|
110
117
|
messages.append({"role": "system", "content": "\n\n".join(system_parts)})
|
|
@@ -120,9 +127,10 @@ def generate_text(messages: list[dict[str, str]], generation: dict[str, Any]) ->
|
|
|
120
127
|
messages,
|
|
121
128
|
tokenize=False,
|
|
122
129
|
add_generation_prompt=True,
|
|
130
|
+
**chat_template_kwargs(BASE_MODEL),
|
|
123
131
|
)
|
|
124
132
|
except Exception as exc:
|
|
125
|
-
raise ValueError(f"
|
|
133
|
+
raise ValueError(f"Model chat-template rendering failed: {exc}") from exc
|
|
126
134
|
inputs = TOKENIZER(prompt, return_tensors="pt")
|
|
127
135
|
if int(inputs["input_ids"].shape[-1]) > MAX_PROMPT_TOKENS:
|
|
128
136
|
raise ValueError(f"Prompt exceeds the {MAX_PROMPT_TOKENS}-token limit.")
|
|
@@ -23,6 +23,7 @@ def build_assistant_only_example(
|
|
|
23
23
|
messages: Any,
|
|
24
24
|
*,
|
|
25
25
|
max_length: int,
|
|
26
|
+
chat_template_kwargs: Mapping[str, Any] | None = None,
|
|
26
27
|
) -> dict[str, list[int]]:
|
|
27
28
|
"""Tokenize one chat row while computing loss only on the final assistant turn.
|
|
28
29
|
|
|
@@ -52,12 +53,14 @@ def build_assistant_only_example(
|
|
|
52
53
|
raise ValueError("the final assistant message must contain non-empty text")
|
|
53
54
|
|
|
54
55
|
try:
|
|
56
|
+
template_kwargs = dict(chat_template_kwargs or {})
|
|
55
57
|
prompt_ids = _token_ids(
|
|
56
58
|
tokenizer.apply_chat_template(
|
|
57
59
|
messages[:-1],
|
|
58
60
|
tokenize=True,
|
|
59
61
|
add_generation_prompt=True,
|
|
60
62
|
return_dict=False,
|
|
63
|
+
**template_kwargs,
|
|
61
64
|
),
|
|
62
65
|
"prompt chat template",
|
|
63
66
|
)
|
|
@@ -67,19 +70,20 @@ def build_assistant_only_example(
|
|
|
67
70
|
tokenize=True,
|
|
68
71
|
add_generation_prompt=False,
|
|
69
72
|
return_dict=False,
|
|
73
|
+
**template_kwargs,
|
|
70
74
|
),
|
|
71
75
|
"full chat template",
|
|
72
76
|
)
|
|
73
77
|
except Exception as exc:
|
|
74
|
-
raise ValueError(f"
|
|
78
|
+
raise ValueError(f"Model chat-template rendering failed: {exc}") from exc
|
|
75
79
|
|
|
76
80
|
if full_ids[: len(prompt_ids)] != prompt_ids:
|
|
77
81
|
raise ValueError(
|
|
78
|
-
"
|
|
82
|
+
"Model chat-template output is not prefix-aligned; refusing to guess the assistant loss boundary"
|
|
79
83
|
)
|
|
80
84
|
completion_ids = full_ids[len(prompt_ids) :]
|
|
81
85
|
if not completion_ids:
|
|
82
|
-
raise ValueError("
|
|
86
|
+
raise ValueError("Model chat template produced no assistant completion tokens")
|
|
83
87
|
if len(completion_ids) >= max_length:
|
|
84
88
|
raise ValueError(
|
|
85
89
|
f"assistant completion requires {len(completion_ids)} tokens, but max_length={max_length}; "
|