@tuned-tensor/cli 0.13.1-beta.6 → 0.13.1-beta.7

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.
@@ -68,6 +68,20 @@ held-out model capability.
68
68
  Do not mix those lockfiles. Each engine hashes its own `pyproject.toml` and
69
69
  `uv.lock` into a distinct cache environment.
70
70
 
71
+ ## Spec-driven instruction contract
72
+
73
+ Adapter and foundation specs extend one engine-neutral behavior schema:
74
+ `name`, `description`, `system_prompt`, `guidelines`, `constraints`, and
75
+ `examples`. Node compiles the three instruction fields into one canonical
76
+ system message. The adapter dataset compiler and every foundation stage use
77
+ that same compiler, so tokenizer input, pretraining smoke data, chat SFT, RL,
78
+ and evaluation all receive guidelines and constraints consistently.
79
+
80
+ Engine-specific fields control execution only. Adapter specs add `base_model`,
81
+ LoRA hyperparameters, and optional prebuilt datasets. Foundation specs add
82
+ `engine: "foundation"` and the `foundation` training configuration. The
83
+ behavioral instruction remains the shared source of truth.
84
+
71
85
  ## Python boundary
72
86
 
73
87
  Node owns input validation, state, process lifecycle, manifests, and reports.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuned-tensor/cli",
3
- "version": "0.13.1-beta.6",
3
+ "version": "0.13.1-beta.7",
4
4
  "description": "Local TT agent and CUDA fine-tuning CLI for Tuned Tensor workflows",
5
5
  "repository": {
6
6
  "type": "git",
@@ -105,6 +105,13 @@ def iter_corpus_documents(path: str | Path, max_chars: int | None = None) -> Ite
105
105
  yield text
106
106
 
107
107
 
108
+ def instruction_corpus_documents(system_prompt: str, documents: Iterable[str]) -> Iterator[str]:
109
+ instruction = system_prompt.strip()
110
+ if instruction:
111
+ yield instruction
112
+ yield from documents
113
+
114
+
108
115
  def corpus_documents_hash(documents: Iterable[str]) -> tuple[str, int]:
109
116
  digest = hashlib.sha256()
110
117
  chars = 0
@@ -141,6 +148,52 @@ def encode_ids(tokenizer: Tokenizer, text: str) -> list[int]:
141
148
  return list(tokenizer.encode(text).ids)
142
149
 
143
150
 
151
+ def encode_prompt_ids(
152
+ tokenizer: Tokenizer,
153
+ system_prompt: str,
154
+ user: str,
155
+ max_tokens: int,
156
+ ) -> list[int]:
157
+ """Keep the complete spec instruction and trim only user content."""
158
+ full_ids = encode_ids(tokenizer, format_prompt(system_prompt, user))
159
+ if len(full_ids) <= max_tokens:
160
+ return full_ids
161
+ system = system_prompt.strip() or "You are a helpful assistant."
162
+ prefix_ids = encode_ids(tokenizer, f"{SYSTEM}{system}{END}{USER}")
163
+ suffix_ids = encode_ids(tokenizer, f"{END}{ASSISTANT}")
164
+ if len(prefix_ids) + len(suffix_ids) > max_tokens:
165
+ raise ValueError(
166
+ "Compiled spec instruction exceeds the foundation model context; "
167
+ "increase foundation.sequence_length or shorten the spec instruction."
168
+ )
169
+ user_ids = encode_ids(tokenizer, user)
170
+ user_budget = max_tokens - len(prefix_ids) - len(suffix_ids)
171
+ retained_user_ids = user_ids[-user_budget:] if user_budget > 0 else []
172
+ return prefix_ids + retained_user_ids + suffix_ids
173
+
174
+
175
+ def encode_generation_prompt(
176
+ tokenizer: Tokenizer,
177
+ system_prompt: str,
178
+ user: str,
179
+ context_length: int,
180
+ requested_completion_tokens: int,
181
+ ) -> tuple[list[int], int]:
182
+ """Reserve completion space without rejecting an instruction that can fit."""
183
+ completion_tokens = min(requested_completion_tokens, max(1, context_length // 2))
184
+ try:
185
+ prompt_ids = encode_prompt_ids(
186
+ tokenizer,
187
+ system_prompt,
188
+ user,
189
+ context_length - completion_tokens,
190
+ )
191
+ except ValueError:
192
+ prompt_ids = encode_prompt_ids(tokenizer, system_prompt, user, context_length - 1)
193
+ completion_tokens = context_length - len(prompt_ids)
194
+ return prompt_ids, completion_tokens
195
+
196
+
144
197
  def decode_ids(tokenizer: Tokenizer, ids: list[int]) -> str:
145
198
  return tokenizer.decode(ids, skip_special_tokens=False)
146
199
 
@@ -168,9 +221,18 @@ def encode_sft_example(
168
221
  full_ids = encode_ids(tokenizer, format_chat(system_prompt, user, assistant))
169
222
  model_tokens = sequence_length + 1
170
223
  if len(full_ids) > model_tokens:
171
- overflow = len(full_ids) - model_tokens
172
- prefix_ids = prefix_ids[overflow:] if overflow < len(prefix_ids) else []
173
- full_ids = full_ids[-model_tokens:]
224
+ assistant_ids = encode_ids(tokenizer, f"{assistant}{END}")
225
+ reserved_assistant_tokens = min(2, len(assistant_ids))
226
+ prefix_ids = encode_prompt_ids(
227
+ tokenizer,
228
+ system_prompt,
229
+ user,
230
+ model_tokens - reserved_assistant_tokens,
231
+ )
232
+ assistant_budget = model_tokens - len(prefix_ids)
233
+ if len(assistant_ids) > assistant_budget:
234
+ assistant_ids = assistant_ids[:assistant_budget - 1] + assistant_ids[-1:]
235
+ full_ids = prefix_ids + assistant_ids
174
236
  input_ids = pad_or_trim(full_ids[:-1], sequence_length, pad_id)
175
237
  target_ids = full_ids[1:]
176
238
  first_assistant_target = max(0, min(len(prefix_ids) - 1, len(target_ids)))
@@ -8,7 +8,7 @@ from tokenizers import Tokenizer
8
8
  from torch.nn import functional as F
9
9
 
10
10
  from common import load_config, write_json
11
- from data import END, decoded_byte_count, encode_ids, example_pairs, format_prompt, iter_corpus_documents, smoke_corpus, window_tokens
11
+ from data import END, decoded_byte_count, encode_generation_prompt, example_pairs, iter_corpus_documents, smoke_corpus, window_tokens
12
12
  from model import bits_per_byte, generate, load_model
13
13
 
14
14
 
@@ -45,12 +45,19 @@ def evaluate_chat(
45
45
  correct = 0
46
46
  model.eval()
47
47
  for example in examples:
48
- prompt_ids = encode_ids(tokenizer, format_prompt(system_prompt, example["input"])) or [0]
48
+ context_length = int(model.config["sequence_length"])
49
+ prompt_ids, completion_tokens = encode_generation_prompt(
50
+ tokenizer,
51
+ system_prompt,
52
+ example["input"],
53
+ context_length,
54
+ 32,
55
+ )
49
56
  tokens = torch.tensor([prompt_ids], dtype=torch.long, device=device)
50
57
  generated = generate(
51
58
  model,
52
59
  tokens,
53
- max_new_tokens=32,
60
+ max_new_tokens=completion_tokens,
54
61
  stop_token_id=tokenizer.token_to_id(END),
55
62
  )[0].tolist()[len(prompt_ids):]
56
63
  text = tokenizer.decode(generated).split(END)[0].strip()
@@ -65,14 +72,27 @@ def evaluate_chat(
65
72
  }
66
73
 
67
74
 
68
- def evaluate_inference(model, tokenizer: Tokenizer, prompt: str, device: torch.device) -> dict[str, float | int | str]:
69
- prompt_ids = encode_ids(tokenizer, prompt) or [0]
70
- tokens = torch.tensor([prompt_ids[: min(16, len(prompt_ids))]], dtype=torch.long, device=device)
75
+ def evaluate_inference(
76
+ model,
77
+ tokenizer: Tokenizer,
78
+ system_prompt: str,
79
+ user_prompt: str,
80
+ device: torch.device,
81
+ ) -> dict[str, float | int | str]:
82
+ context_length = int(model.config["sequence_length"])
83
+ prompt_ids, completion_tokens = encode_generation_prompt(
84
+ tokenizer,
85
+ system_prompt,
86
+ user_prompt,
87
+ context_length,
88
+ 32,
89
+ )
90
+ tokens = torch.tensor([prompt_ids], dtype=torch.long, device=device)
71
91
  if device.type == "cuda":
72
92
  torch.cuda.reset_peak_memory_stats(device)
73
93
  torch.cuda.synchronize()
74
94
  started = time.perf_counter()
75
- generated = generate(model, tokens, max_new_tokens=32)
95
+ generated = generate(model, tokens, max_new_tokens=completion_tokens)
76
96
  if device.type == "cuda":
77
97
  torch.cuda.synchronize()
78
98
  elapsed = max(time.perf_counter() - started, 1e-6)
@@ -112,7 +132,13 @@ def main(argv: list[str] | None = None) -> None:
112
132
  metrics.update(evaluate_chat(model, tokenizer, str(config.get("system_prompt") or ""), examples, device))
113
133
  elif evaluator == "inference":
114
134
  prompt = examples[0]["input"] if examples else "hello"
115
- metrics.update(evaluate_inference(model, tokenizer, prompt, device))
135
+ metrics.update(evaluate_inference(
136
+ model,
137
+ tokenizer,
138
+ str(config.get("system_prompt") or ""),
139
+ prompt,
140
+ device,
141
+ ))
116
142
  else:
117
143
  raise SystemExit(f"Unknown foundation evaluator: {evaluator}")
118
144
  write_json(output_dir / "metrics.json", metrics)
@@ -16,7 +16,15 @@ from torch.nn import functional as F
16
16
 
17
17
  from checkpoint import CheckpointManager
18
18
  from common import append_jsonl, ensure_private_directory, load_config, make_private_file, require_cuda, write_json
19
- from data import END, corpus_hash, corpus_manifest, example_pairs, iter_corpus_documents, smoke_corpus
19
+ from data import (
20
+ END,
21
+ corpus_hash,
22
+ corpus_manifest,
23
+ example_pairs,
24
+ instruction_corpus_documents,
25
+ iter_corpus_documents,
26
+ smoke_corpus,
27
+ )
20
28
  from model import FoundationGPT, model_config_from_depth, param_count, save_model
21
29
 
22
30
 
@@ -37,10 +45,12 @@ def file_sha256(path: str | Path) -> str:
37
45
  def source_signature(config: dict[str, Any]) -> dict[str, Any]:
38
46
  corpus_path = config.get("corpus_path")
39
47
  if corpus_path:
48
+ instruction = str(config.get("system_prompt") or "").strip()
40
49
  return {
41
50
  "kind": "files",
42
51
  "manifest": corpus_manifest(corpus_path),
43
52
  "max_chars": int(config.get("max_chars") or 0),
53
+ "instruction_sha256": corpus_hash(instruction),
44
54
  }
45
55
  examples = example_pairs(config.get("examples") or [])
46
56
  corpus = smoke_corpus(
@@ -142,7 +152,10 @@ def prepare_token_cache(
142
152
  recorded_bytes = 0
143
153
  corpus_path = config.get("corpus_path")
144
154
  if corpus_path:
145
- documents = iter_corpus_documents(corpus_path, int(config.get("max_chars") or 0))
155
+ documents = instruction_corpus_documents(
156
+ str(config.get("system_prompt") or ""),
157
+ iter_corpus_documents(corpus_path, int(config.get("max_chars") or 0)),
158
+ )
146
159
  else:
147
160
  examples = example_pairs(config.get("examples") or [])
148
161
  documents = iter([smoke_corpus(
@@ -7,7 +7,7 @@ from tokenizers import Tokenizer
7
7
  from torch.nn import functional as F
8
8
 
9
9
  from common import load_config, require_cuda, write_json
10
- from data import END, encode_ids, example_pairs, format_prompt, numeric_reward
10
+ from data import END, encode_generation_prompt, example_pairs, numeric_reward
11
11
  from model import FoundationGPT, load_model, param_count, save_model
12
12
 
13
13
 
@@ -73,10 +73,18 @@ def main(argv: list[str] | None = None) -> None:
73
73
  last_loss = 0.0
74
74
  for step in range(steps):
75
75
  example = examples[step % len(examples)]
76
- prompt_ids = encode_ids(tokenizer, format_prompt(str(config.get("system_prompt") or ""), example["input"]))
76
+ context_length = int(model.config["sequence_length"])
77
+ prompt_ids, completion_tokens = encode_generation_prompt(
78
+ tokenizer,
79
+ str(config.get("system_prompt") or ""),
80
+ example["input"],
81
+ context_length,
82
+ 24,
83
+ )
77
84
  prompt_ids, completion_tokens = bounded_rollout(
78
85
  prompt_ids,
79
- int(model.config["sequence_length"]),
86
+ context_length,
87
+ completion_tokens,
80
88
  )
81
89
  prompt = torch.tensor([prompt_ids], dtype=torch.long, device=device)
82
90
  sampled = sample_rollout(
@@ -6,6 +6,7 @@ from data import (
6
6
  corpus_hash,
7
7
  corpus_manifest,
8
8
  example_pairs,
9
+ instruction_corpus_documents,
9
10
  iter_corpus_documents,
10
11
  smoke_corpus,
11
12
  train_tokenizer,
@@ -20,9 +21,17 @@ def main(argv: list[str] | None = None) -> None:
20
21
  corpus_path = config.get("corpus_path")
21
22
  if corpus_path:
22
23
  manifest_before = corpus_manifest(corpus_path)
23
- corpus_sha256, corpus_chars = corpus_documents_hash(iter_corpus_documents(corpus_path, max_chars))
24
+ system_prompt = str(config.get("system_prompt") or "")
25
+
26
+ def documents():
27
+ return instruction_corpus_documents(
28
+ system_prompt,
29
+ iter_corpus_documents(corpus_path, max_chars),
30
+ )
31
+
32
+ corpus_sha256, corpus_chars = corpus_documents_hash(documents())
24
33
  tokenizer = train_tokenizer(
25
- iter_corpus_documents(corpus_path, max_chars),
34
+ documents(),
26
35
  int(config.get("vocab_size") or 256),
27
36
  )
28
37
  if corpus_manifest(corpus_path) != manifest_before: