@tuned-tensor/cli 0.15.1 → 0.17.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.
- package/README.md +124 -46
- package/dist/{chunk-VVKC563R.js → chunk-KRKISPHG.js} +420 -98
- package/dist/chunk-KRKISPHG.js.map +1 -0
- package/dist/index.js +1070 -706
- package/dist/index.js.map +1 -1
- package/dist/local-runtime.js +147 -32
- package/dist/local-runtime.js.map +1 -1
- package/docs/local-runtime/architecture.md +17 -6
- package/docs/local-runtime/aws-gpu.md +248 -0
- package/docs/local-runtime/foundation-long-runs.md +21 -5
- package/docs/local-runtime/serving.md +11 -1
- package/docs/spec-architecture.html +573 -0
- package/docs/spec-workflow.md +110 -0
- package/examples/local-runtime/aws-runner.json +13 -0
- package/package.json +6 -4
- package/training/adapter/src/foundation_export.py +81 -0
- package/training/adapter/src/serve.py +12 -5
- package/training/adapter/src/train.py +4 -0
- package/training/foundation/pyproject.toml +4 -0
- package/training/foundation/src/model.py +2 -0
- package/training/foundation/uv.lock +225 -0
- package/dist/chunk-VVKC563R.js.map +0 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Work from the behavior spec
|
|
2
|
+
|
|
3
|
+
Explore the [interactive architecture diagram](spec-architecture.html) for source
|
|
4
|
+
excerpts, code structure and command examples. Open the HTML file in a browser.
|
|
5
|
+
|
|
6
|
+
`tunedtensor.json` is the local recipe for the model's behavior, examples and
|
|
7
|
+
training settings. Ask the agent to improve that recipe, review the proposed
|
|
8
|
+
diff, approve the edit, then preview or execute a pipeline against the saved spec.
|
|
9
|
+
The agent reads the actual file before proposing changes; it does not use the
|
|
10
|
+
conversation transcript as a replacement for the current spec.
|
|
11
|
+
|
|
12
|
+
## Review without a model call
|
|
13
|
+
|
|
14
|
+
Inside the TT shell, in your project directory:
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
/spec
|
|
18
|
+
/spec validate
|
|
19
|
+
/spec diff
|
|
20
|
+
/spec history
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`/spec` shows the complete current file, validation results and SHA-256.
|
|
24
|
+
`/spec diff` shows pending spec edits in the current conversation; otherwise it
|
|
25
|
+
shows the latest approved edit, or external changes since that revision.
|
|
26
|
+
`/spec history` lists up to 50 recent revision records. These commands require
|
|
27
|
+
no credentials and work before the agent is configured.
|
|
28
|
+
|
|
29
|
+
Outside the shell, use `tt spec show`, `tt spec validate`, `tt spec diff`, and
|
|
30
|
+
`tt spec history`. Add a workspace-relative path, for example
|
|
31
|
+
`tt spec show feedback/tunedtensor.json`. For continued work on a nested
|
|
32
|
+
project, `/cd feedback` makes its `tunedtensor.json` the current recipe.
|
|
33
|
+
`--json` returns structured review results. `tt spec validate` exits nonzero
|
|
34
|
+
for an invalid spec.
|
|
35
|
+
|
|
36
|
+
## Edit, review, approve
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
› Keep my training settings, but require a single lowercase sentiment label.
|
|
40
|
+
[agent reads the spec and proposes a diff]
|
|
41
|
+
› /spec diff
|
|
42
|
+
› /approve
|
|
43
|
+
› /spec
|
|
44
|
+
› /spec history
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`get_local_spec` returns the document, validation diagnostics and content hash.
|
|
48
|
+
`prepare_update_local_spec` requires that hash and a patch of supported fields.
|
|
49
|
+
Arrays replace their whole field; `hyperparameters` and `foundation` merge by
|
|
50
|
+
key. Unmentioned fields remain unchanged. Identity and engine cannot change
|
|
51
|
+
through this edit tool. The full resulting document must pass the runtime's
|
|
52
|
+
strict schema, readiness checks, nonblank content checks and distinct-example
|
|
53
|
+
checks. Small example sets produce a warning; validation is not a claim of model
|
|
54
|
+
quality or GPU readiness.
|
|
55
|
+
|
|
56
|
+
Proposing an edit does not write anything. `/approve` revalidates the workspace,
|
|
57
|
+
parent directory, current file bytes and proposed result, acquires the spec's
|
|
58
|
+
writer lock and atomically replaces the file. `/reject` leaves it unchanged.
|
|
59
|
+
If the file changes after review, prepare a fresh edit. A saved edit invalidates
|
|
60
|
+
any previously prepared pipeline preview for the old spec.
|
|
61
|
+
|
|
62
|
+
Secure agent edits currently require Linux filesystem handles. Reads work on
|
|
63
|
+
other supported CLI platforms; edit the JSON with your editor there. Spec reads
|
|
64
|
+
and edits are bounded to 200 KB. Large model-facing proposals may hit the
|
|
65
|
+
agent's existing smaller review budget; split the request or use your editor.
|
|
66
|
+
|
|
67
|
+
Revision records live beside the spec under `.tuned-tensor/spec-history/` and
|
|
68
|
+
contain before/after snapshots and hashes. They can contain your training
|
|
69
|
+
examples. They use private permissions. History records changes made through
|
|
70
|
+
this edit workflow; it is not a file watcher or a complete history of external
|
|
71
|
+
editor changes. A `prepared` record without an `applied` record is an incomplete
|
|
72
|
+
journal entry: inspect the current file and its hash before attempting another
|
|
73
|
+
edit. A crashed writer can leave `.tunedtensor-spec.lock`; after confirming no
|
|
74
|
+
TT writer is still running, remove that lock manually. The lock coordinates TT
|
|
75
|
+
writers; avoid concurrent external editor saves during approval.
|
|
76
|
+
|
|
77
|
+
## Execute the saved recipe
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
tt spec validate
|
|
81
|
+
tt pipeline run --spec tunedtensor.json --dry-run
|
|
82
|
+
tt pipeline run --spec tunedtensor.json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The default pipeline derives from the current spec. An adjacent
|
|
86
|
+
`tunedtensor.pipeline.json` is no longer loaded implicitly. To select an advanced
|
|
87
|
+
recipe explicitly, pass `--file`; a missing explicit recipe or `--spec` file fails. Foundation
|
|
88
|
+
training parameters in an explicit recipe must agree with the spec, including
|
|
89
|
+
in previews and the runtime's own execution guard. Change the spec and regenerate
|
|
90
|
+
the recipe when they disagree. Explicit recipes may still select supported
|
|
91
|
+
stages; `--only` and `--skip` remain available.
|
|
92
|
+
|
|
93
|
+
CLI planning, validation, execution and agent pipeline proposals share
|
|
94
|
+
`pipelineForRunInput` in `src/pipeline.ts`. It uses the same
|
|
95
|
+
`validateBehaviorSpec` rules as spec review and editing, defined beside the
|
|
96
|
+
runtime parser in `src/local-runtime/local-project.ts`. The edit module only
|
|
97
|
+
owns workspace access, patches and revision history.
|
|
98
|
+
|
|
99
|
+
The command reads one spec snapshot for planning, hardware warnings and execution. Plan/dry-run
|
|
100
|
+
JSON and final result JSON identify its path and SHA-256. The spec's compiled
|
|
101
|
+
instructions and examples feed training and task evaluation; the independently
|
|
102
|
+
configured general-regression suite keeps its own prompt. Provider choice and
|
|
103
|
+
GPU placement remain separate runtime choices in agent configuration and
|
|
104
|
+
`local-runner.json`. Evaluation policy (inference/scoring settings and the
|
|
105
|
+
general-regression suite) also remains in the runner config. Moving that policy
|
|
106
|
+
into the spec would be a separate schema change.
|
|
107
|
+
|
|
108
|
+
Agent `/approve` for a pipeline still runs a preview only. Real training uses
|
|
109
|
+
the direct command above. The spec is the behavior recipe; approval of a spec
|
|
110
|
+
edit is not approval to launch training.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"artifactRoot": ".tuned-tensor/artifacts",
|
|
3
|
+
"storeRoot": ".tuned-tensor/store",
|
|
4
|
+
"gpu": {
|
|
5
|
+
"provider": "aws",
|
|
6
|
+
"profile": "research",
|
|
7
|
+
"region": "eu-west-1",
|
|
8
|
+
"instanceId": "i-0123456789abcdef0",
|
|
9
|
+
"user": "ubuntu",
|
|
10
|
+
"identityFile": "~/.ssh/research-gpu.pem",
|
|
11
|
+
"maxSeconds": 86400
|
|
12
|
+
}
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuned-tensor/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Local TT agent and CUDA fine-tuning CLI for Tuned Tensor workflows",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"test:workflows": "vitest run src/__tests__/workflows",
|
|
20
20
|
"eval:agent": "tsx scripts/eval-agent.ts",
|
|
21
21
|
"test:local-runtime": "node --import tsx --test \"test/local-runtime/**/*.test.ts\"",
|
|
22
|
-
"test:python": "python3 -m unittest discover -s training/adapter/tests -p 'test_*.py' && uv run --frozen --project training/foundation python -m unittest discover -s training/foundation/tests -p 'test_*.py'",
|
|
22
|
+
"test:python": "python3 -m unittest discover -s training/adapter/tests -p 'test_*.py' && uv run --frozen --project training/foundation --group test python -m unittest discover -s training/foundation/tests -p 'test_*.py'",
|
|
23
23
|
"test:watch": "vitest",
|
|
24
24
|
"check": "npm run typecheck && npm test && npm run build"
|
|
25
25
|
},
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
"training/foundation/uv.lock",
|
|
35
35
|
"training/foundation/src/*.py",
|
|
36
36
|
"docs/local-runtime",
|
|
37
|
+
"docs/spec-workflow.md",
|
|
38
|
+
"docs/spec-architecture.html",
|
|
37
39
|
"examples/local-runtime/*.json",
|
|
38
40
|
"examples/local-runtime/*.jsonl"
|
|
39
41
|
],
|
|
@@ -53,9 +55,9 @@
|
|
|
53
55
|
"@earendil-works/pi-agent-core": "0.84.1",
|
|
54
56
|
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
55
57
|
"@tuned-tensor/pipeline-contract": "1.1.0",
|
|
56
|
-
"chalk": "^
|
|
58
|
+
"chalk": "^6.0.0",
|
|
57
59
|
"cli-table3": "^0.6.5",
|
|
58
|
-
"commander": "^
|
|
60
|
+
"commander": "^15.0.0",
|
|
59
61
|
"open": "^10.1.0",
|
|
60
62
|
"ora": "^9.3.0",
|
|
61
63
|
"typebox": "1.3.7",
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Export TT FoundationGPT weights to vLLM's existing GPT-2 implementation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
CHAT_TEMPLATE = """{%- if not messages or messages[0]['role'] != 'system' -%}
|
|
8
|
+
{{- '<|system|>You are a helpful assistant.<|end|>' -}}
|
|
9
|
+
{%- endif -%}
|
|
10
|
+
{%- for message in messages -%}
|
|
11
|
+
{%- set content = message['content'] -%}
|
|
12
|
+
{%- if message['role'] == 'system' -%}
|
|
13
|
+
{%- set content = (content | trim) or 'You are a helpful assistant.' -%}
|
|
14
|
+
{%- endif -%}
|
|
15
|
+
{%- if message['role'] not in ['system', 'user', 'assistant'] -%}
|
|
16
|
+
{{- raise_exception('Foundation chat supports system, user and assistant messages only.') -}}
|
|
17
|
+
{%- endif -%}
|
|
18
|
+
{{- '<|' + message['role'] + '|>' + content + '<|end|>' -}}
|
|
19
|
+
{%- endfor -%}
|
|
20
|
+
{%- if add_generation_prompt -%}{{- '<|assistant|>' -}}{%- endif -%}"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def export_foundation(checkpoint: str, tokenizer_path: str, destination: Path) -> str:
|
|
24
|
+
import torch
|
|
25
|
+
from safetensors.torch import load_file, save_file
|
|
26
|
+
from tokenizers import Tokenizer
|
|
27
|
+
from transformers import GPT2Config, PreTrainedTokenizerFast
|
|
28
|
+
|
|
29
|
+
source = Path(checkpoint)
|
|
30
|
+
config = json.loads((source / 'config.json').read_text())
|
|
31
|
+
for key in ('depth', 'width', 'heads', 'vocab_size', 'sequence_length'):
|
|
32
|
+
if type(config.get(key)) is not int or config[key] <= 0:
|
|
33
|
+
raise ValueError(f'Invalid foundation config: {key}')
|
|
34
|
+
if config['width'] % config['heads']:
|
|
35
|
+
raise ValueError('Foundation width must be divisible by heads.')
|
|
36
|
+
raw_tokenizer = Tokenizer.from_file(tokenizer_path)
|
|
37
|
+
special_tokens = ['<unk>', '<pad>', '<|system|>', '<|user|>', '<|assistant|>', '<|end|>']
|
|
38
|
+
if any(raw_tokenizer.token_to_id(token) is None for token in special_tokens):
|
|
39
|
+
raise ValueError('Foundation tokenizer is missing required special tokens.')
|
|
40
|
+
tokenizer = PreTrainedTokenizerFast(tokenizer_object=raw_tokenizer,
|
|
41
|
+
unk_token='<unk>', pad_token='<pad>', eos_token='<|end|>',
|
|
42
|
+
additional_special_tokens=['<|system|>', '<|user|>', '<|assistant|>'],
|
|
43
|
+
model_max_length=config['sequence_length'], chat_template=CHAT_TEMPLATE)
|
|
44
|
+
if len(tokenizer) != config['vocab_size']:
|
|
45
|
+
raise ValueError('Foundation tokenizer vocabulary does not match the checkpoint.')
|
|
46
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
tokenizer.save_pretrained(destination)
|
|
48
|
+
GPT2Config(n_layer=config['depth'], n_embd=config['width'], n_head=config['heads'],
|
|
49
|
+
n_positions=config['sequence_length'], vocab_size=config['vocab_size'],
|
|
50
|
+
activation_function='gelu', layer_norm_epsilon=1e-5,
|
|
51
|
+
resid_pdrop=0.0, embd_pdrop=0.0, attn_pdrop=0.0,
|
|
52
|
+
bos_token_id=None, eos_token_id=tokenizer.eos_token_id,
|
|
53
|
+
pad_token_id=tokenizer.pad_token_id, architectures=['GPT2LMHeadModel'],
|
|
54
|
+
tie_word_embeddings=True).save_pretrained(destination)
|
|
55
|
+
weights = load_file(str(source / 'model.safetensors'))
|
|
56
|
+
exported = {}
|
|
57
|
+
def move(old, new, transpose=False):
|
|
58
|
+
tensor = weights.pop(old)
|
|
59
|
+
exported[new] = tensor.t().contiguous() if transpose else tensor
|
|
60
|
+
move('tok_emb.weight', 'transformer.wte.weight')
|
|
61
|
+
move('pos_emb.weight', 'transformer.wpe.weight')
|
|
62
|
+
head = weights.pop('lm_head.weight')
|
|
63
|
+
if not torch.equal(head, exported['transformer.wte.weight']):
|
|
64
|
+
raise ValueError('Foundation checkpoint must have tied embedding weights.')
|
|
65
|
+
for suffix in ('weight', 'bias'):
|
|
66
|
+
move('ln_f.' + suffix, 'transformer.ln_f.' + suffix)
|
|
67
|
+
for i in range(config['depth']):
|
|
68
|
+
old, new = f'blocks.{i}.', f'transformer.h.{i}.'
|
|
69
|
+
for src, dst in [('ln1', 'ln_1'), ('ln2', 'ln_2'),
|
|
70
|
+
('mlp.0', 'mlp.c_fc'), ('mlp.2', 'mlp.c_proj')]:
|
|
71
|
+
for suffix in ('weight', 'bias'):
|
|
72
|
+
move(old + src + '.' + suffix, new + dst + '.' + suffix,
|
|
73
|
+
transpose=src.startswith('mlp') and suffix == 'weight')
|
|
74
|
+
for src, dst in [('attn.qkv', 'attn.c_attn'), ('attn.proj', 'attn.c_proj')]:
|
|
75
|
+
move(old + src + '.weight', new + dst + '.weight', transpose=True)
|
|
76
|
+
weight = exported[new + dst + '.weight']
|
|
77
|
+
exported[new + dst + '.bias'] = torch.zeros(weight.shape[1], dtype=weight.dtype)
|
|
78
|
+
if weights:
|
|
79
|
+
raise ValueError(f'Unexpected foundation weights: {sorted(weights)}')
|
|
80
|
+
save_file(exported, str(destination / 'model.safetensors'), metadata={'format': 'pt'})
|
|
81
|
+
return str(destination)
|
|
@@ -64,9 +64,10 @@ def build_vllm_args(model_source: str, adapter_path: str | None, temp_dir: Path)
|
|
|
64
64
|
"Qwen/Qwen3.5-2B": "qwen3_xml",
|
|
65
65
|
"nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16": "qwen3_coder",
|
|
66
66
|
"meta-models/Muse-Glimmer-30B": "muse_glimmer",
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
}.get(base)
|
|
68
|
+
if parser:
|
|
69
|
+
args += ["--enable-auto-tool-choice", "--tool-call-parser", parser]
|
|
70
|
+
if parser and base != "meta-models/Muse-Glimmer-30B":
|
|
70
71
|
args += ["--default-chat-template-kwargs", '{"enable_thinking":false}']
|
|
71
72
|
if adapter_path:
|
|
72
73
|
config = json.loads((Path(adapter_path) / "adapter_config.json").read_text())
|
|
@@ -124,11 +125,17 @@ def run_server() -> None:
|
|
|
124
125
|
family = socket.AF_INET6 if ":" in host else socket.AF_INET
|
|
125
126
|
with socket.socket(family, socket.SOCK_STREAM) as probe:
|
|
126
127
|
probe.bind((host, port))
|
|
127
|
-
model_source = prepare_model_source()
|
|
128
128
|
if os.environ.get("TT_API_KEY"):
|
|
129
129
|
os.environ["VLLM_API_KEY"] = os.environ["TT_API_KEY"]
|
|
130
130
|
with TemporaryDirectory(prefix="tt-serve-") as temp:
|
|
131
|
-
|
|
131
|
+
if os.environ.get("TT_FOUNDATION_CHECKPOINT"):
|
|
132
|
+
from foundation_export import export_foundation
|
|
133
|
+
model_source = export_foundation(os.environ["TT_FOUNDATION_CHECKPOINT"],
|
|
134
|
+
os.environ["TT_FOUNDATION_TOKENIZER"], Path(temp) / "foundation")
|
|
135
|
+
adapter = None
|
|
136
|
+
else:
|
|
137
|
+
model_source = prepare_model_source()
|
|
138
|
+
adapter = resolve_adapter_path(os.environ.get("TT_MODEL_ARTIFACT"), Path(temp))
|
|
132
139
|
args = build_vllm_args(model_source, adapter, Path(temp))
|
|
133
140
|
from vllm.entrypoints.cli.main import main
|
|
134
141
|
previous = sys.argv
|
|
@@ -282,6 +282,10 @@ def run_training(rows: list[dict[str, Any]], model_source: str) -> tuple[dict[st
|
|
|
282
282
|
)
|
|
283
283
|
result = trainer.train()
|
|
284
284
|
|
|
285
|
+
# Saved adapters must remain loadable after a local or remote staging directory is removed.
|
|
286
|
+
for adapter_config in model.peft_config.values():
|
|
287
|
+
adapter_config.base_model_name_or_path = str(hp("base_model", CERTIFIED_BASE_MODEL))
|
|
288
|
+
adapter_config.revision = hp("base_model_revision")
|
|
285
289
|
model.save_pretrained(MODEL_DIR)
|
|
286
290
|
tokenizer.save_pretrained(MODEL_DIR)
|
|
287
291
|
return result.metrics, dtype
|
|
@@ -10,6 +10,10 @@ dependencies = [
|
|
|
10
10
|
"torch>=2.11.0",
|
|
11
11
|
]
|
|
12
12
|
|
|
13
|
+
[dependency-groups]
|
|
14
|
+
# Match the pinned serving runtime for CPU export parity tests.
|
|
15
|
+
test = ["transformers==5.16.1"]
|
|
16
|
+
|
|
13
17
|
[tool.uv.sources]
|
|
14
18
|
torch = [
|
|
15
19
|
{ index = "pytorch-cu130", marker = "sys_platform == 'linux'" },
|
|
@@ -132,6 +132,8 @@ def generate(
|
|
|
132
132
|
def save_model(model: FoundationGPT, directory: str | Path) -> None:
|
|
133
133
|
destination = ensure_private_directory(directory)
|
|
134
134
|
tensors = {key: value.detach().cpu().contiguous() for key, value in model.state_dict().items()}
|
|
135
|
+
# CPU tensors can still alias the tied embedding after .cpu().
|
|
136
|
+
tensors["lm_head.weight"] = tensors["lm_head.weight"].clone()
|
|
135
137
|
model_path = destination / "model.safetensors"
|
|
136
138
|
save_file(tensors, str(model_path))
|
|
137
139
|
make_private_file(model_path)
|