@tuned-tensor/cli 0.11.0 → 0.12.0-beta.0

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.
@@ -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 the local workflow's first supported path:
8
- `Qwen/Qwen3.5-2B` text SFT with a LoRA adapter.
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.11.0",
3
+ "version": "0.12.0-beta.0",
4
4
  "description": "Local Pi 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.13.0",
8
+ "peft>=0.20.0",
9
9
  "safetensors>=0.4.5",
10
10
  "torch>=2.11.0",
11
- "transformers>=4.57.0",
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"Qwen chat-template rendering failed: {exc}") from exc
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 != CERTIFIED_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 currently certifies only {CERTIFIED_BASE_MODEL}; got {base_model!r}"
148
+ f"The bundled evaluator does not certify {base_model!r}"
141
149
  )
142
150
 
143
151
 
144
- def load_text_model(payload: dict[str, Any], adapter_path: str | None):
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
- _assert_certified_model_source(base_model)
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(base_model).exists():
169
+ if not Path(model_source).exists():
155
170
  source_kwargs["local_files_only"] = True
156
- if revision and not Path(base_model).exists():
171
+ if revision and not Path(model_source).exists():
157
172
  source_kwargs["revision"] = revision
158
173
 
159
- tokenizer = AutoTokenizer.from_pretrained(base_model, **source_kwargs)
174
+ tokenizer = AutoTokenizer.from_pretrained(model_source, **source_kwargs)
160
175
  if tokenizer.eos_token_id is None:
161
- raise ValueError(f"{CERTIFIED_BASE_MODEL} tokenizer has no EOS token")
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
- model = AutoModelForCausalLM.from_pretrained(base_model, **model_kwargs)
176
- assert_certified_model_config(model.config)
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 Qwen SFT evaluator is text-only and does not accept input_assets")
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
- if payload.get("model_loader") not in (None, "causal_lm"):
239
- raise ValueError("The bundled evaluator is text-only and requires model_loader=causal_lm")
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
- CERTIFIED_TEXT_CONFIG = {
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(value: Any, label: str = "base-model config") -> None:
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
- if model_type == "qwen3_5":
28
- text_config = _field(value, "text_config")
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 "Qwen3_5ForConditionalGeneration" not in architectures
32
- or text_config is None
94
+ or "NemotronHForCausalLM" not in architectures
33
95
  ):
34
- raise ValueError(f"{label} is not the certified {CERTIFIED_BASE_MODEL} architecture")
35
- elif model_type == "qwen3_5_text":
36
- # AutoModelForCausalLM exposes the selected text sub-config after it
37
- # dispatches the verified repository-level Qwen3.5 config.
38
- text_config = value
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 the certified {CERTIFIED_BASE_MODEL} architecture")
41
- for name, expected in CERTIFIED_TEXT_CONFIG.items():
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 {CERTIFIED_BASE_MODEL} architecture: "
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
- CERTIFIED_BASE_MODEL,
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(parsed_config, f"Cached config {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
- if base_model != CERTIFIED_BASE_MODEL:
148
- raise ValueError(
149
- f"The bundled trainer currently certifies only {CERTIFIED_BASE_MODEL}; got {base_model!r}"
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 != "causal_lm":
46
- raise ValueError("The bundled model server is text-only and requires TT_MODEL_LOADER=causal_lm")
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 Qwen model server accepts text content only.")
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
- # Qwen permits one leading system message. Merge the invariant owner
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"Qwen chat-template rendering failed: {exc}") from exc
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"Qwen chat-template rendering failed: {exc}") from exc
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
- "Qwen chat-template output is not prefix-aligned; refusing to guess the assistant loss boundary"
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("Qwen chat template produced no assistant completion tokens")
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}; "