@tuned-tensor/cli 0.24.0 → 0.26.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.
@@ -137,6 +137,16 @@ def verify_snapshot(snapshot: Path, expected_model_id: str | None = None) -> tup
137
137
  return files, verified_blobs
138
138
 
139
139
 
140
+ def check_requested_revision(base_model: str, revision: str | None, resolve_latest: bool) -> None:
141
+ """Certified models keep their reviewed revision. An uncertified model needs
142
+ a pinned commit, except for a direct download that resolves its latest
143
+ commit (`tt models prefetch --repo`); the resolved SHA is reported so the
144
+ spec can pin it."""
145
+ if resolve_latest and revision is None and not is_certified_base_model(base_model):
146
+ return
147
+ assert_certified_base_model_revision(base_model, revision, "Prefetch base model revision")
148
+
149
+
140
150
  def main() -> None:
141
151
  parser = argparse.ArgumentParser(description="Prefetch a TT Hugging Face base model for local use.")
142
152
  parser.add_argument("--input", required=True)
@@ -147,11 +157,7 @@ def main() -> None:
147
157
  base_model = str(payload["base_model"])
148
158
  assert_certified_base_model(base_model, "Prefetch base model")
149
159
  requested_revision = payload.get("revision")
150
- assert_certified_base_model_revision(
151
- base_model,
152
- requested_revision,
153
- "Prefetch base model revision",
154
- )
160
+ check_requested_revision(base_model, requested_revision, bool(payload.get("resolve_latest")))
155
161
  cache_dir = payload.get("model_cache")
156
162
  configure_hugging_face_cache(cache_dir)
157
163
 
@@ -63,6 +63,35 @@ def pin_native_content_format(args: list[str], model_source: str) -> None:
63
63
  args += ["--chat-template-content-format", content_format]
64
64
 
65
65
 
66
+ def prepare_serving_adapter(adapter_path: str, temp_dir: Path) -> str:
67
+ # Transformers trains Qwen's text-only model, but vLLM loads the repository's
68
+ # conditional architecture, even with --language-model-only. Its LoRA mapper
69
+ # expects the language_model prefix; otherwise it silently misses our layers.
70
+ if os.environ.get("TT_BASE_MODEL") != "Qwen/Qwen3.5-2B":
71
+ return adapter_path
72
+ from safetensors import safe_open
73
+ from safetensors.torch import save_file
74
+
75
+ source = Path(adapter_path)
76
+ old = "base_model.model.model.layers."
77
+ new = "base_model.model.model.language_model.layers."
78
+ with safe_open(str(source / "adapter_model.safetensors"), framework="pt", device="cpu") as weights:
79
+ if not any(key.startswith(old) for key in weights.keys()):
80
+ return adapter_path
81
+ tensors = {}
82
+ for key in weights.keys():
83
+ mapped = new + key[len(old):] if key.startswith(old) else key
84
+ if mapped in tensors:
85
+ raise ValueError("Adapter contains conflicting text and conditional layer names.")
86
+ tensors[mapped] = weights.get_tensor(key)
87
+ destination = temp_dir / "serving-adapter"
88
+ destination.mkdir()
89
+ save_file(tensors, str(destination / "adapter_model.safetensors"), metadata=weights.metadata())
90
+ # Keep the verified archive and extracted training layout unchanged.
91
+ (destination / "adapter_config.json").write_bytes((source / "adapter_config.json").read_bytes())
92
+ return str(destination)
93
+
94
+
66
95
  def build_vllm_args(model_source: str, adapter_path: str | None, temp_dir: Path) -> list[str]:
67
96
  args = [
68
97
  "serve", model_source,
@@ -101,6 +130,7 @@ def build_vllm_args(model_source: str, adapter_path: str | None, temp_dir: Path)
101
130
  adapter_base = config.get("base_model_name_or_path")
102
131
  if adapter_base not in {os.environ["TT_BASE_MODEL"], model_source}:
103
132
  raise ValueError("The adapter base does not match the verified serving model.")
133
+ adapter_path = prepare_serving_adapter(adapter_path, temp_dir)
104
134
  base_name = "base:" + os.environ["TT_BASE_MODEL"]
105
135
  args[args.index("--served-model-name") + 1] = base_name
106
136
  args += ["--enable-lora", "--max-lora-rank", str(max(8, int(config["r"]))),
@@ -26,6 +26,7 @@ from model_contract import (
26
26
  assert_certified_base_model_revision,
27
27
  assert_certified_model_config,
28
28
  chat_template_kwargs,
29
+ cuda_model_load_device_map,
29
30
  parse_lora_target_modules,
30
31
  uncertified_load_kwargs,
31
32
  )
@@ -216,13 +217,16 @@ def create_model_and_tokenizer(model_source: str) -> tuple[Any, Any, torch.dtype
216
217
  if hp("model_loader", "causal_lm") == "image_text_to_text"
217
218
  else AutoModelForCausalLM
218
219
  )
220
+ load_device_map = cuda_model_load_device_map(torch)
219
221
  model = model_class.from_pretrained(
220
222
  model_source,
221
223
  **source_kwargs,
222
224
  **uncertified_load_kwargs(str(hp("base_model", CERTIFIED_BASE_MODEL))),
223
225
  dtype=dtype,
224
- device_map={"": torch.cuda.current_device()},
226
+ device_map=load_device_map,
225
227
  )
228
+ if load_device_map is None:
229
+ model = model.to("cuda")
226
230
  assert_certified_model_config(
227
231
  model.config,
228
232
  expected_model_id=str(hp("base_model", CERTIFIED_BASE_MODEL)),