@tuned-tensor/cli 0.13.1-beta.7 → 0.14.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 +46 -19
- package/dist/{chunk-AKJ5WZ2S.js → chunk-VRDDO2FP.js} +2407 -2387
- package/dist/chunk-VRDDO2FP.js.map +1 -0
- package/dist/index.js +766 -301
- package/dist/index.js.map +1 -1
- package/dist/local-runtime.js +120 -25
- package/dist/local-runtime.js.map +1 -1
- package/docs/local-runtime/architecture.md +2 -2
- package/docs/local-runtime/serving.md +168 -0
- package/docs/local-runtime/spark.md +1 -1
- package/package.json +3 -1
- package/training/adapter/src/serve.py +133 -267
- package/training/serving/pyproject.toml +9 -0
- package/training/serving/uv.lock +2785 -0
- package/dist/chunk-AKJ5WZ2S.js.map +0 -1
|
@@ -43,9 +43,9 @@ resource defaults, data contract, and real CUDA acceptance test.
|
|
|
43
43
|
TT Local currently ships two such methods:
|
|
44
44
|
|
|
45
45
|
- **Adapter** (`training/adapter`): certified Transformers checkpoints and
|
|
46
|
-
PEFT LoRA SFT. Driven by `tt run
|
|
46
|
+
PEFT LoRA SFT. Driven by `tt pipeline run`.
|
|
47
47
|
- **Foundation** (`training/foundation`): a from-scratch BPE tokenizer and GPT
|
|
48
|
-
with no Transformers/PEFT. Driven
|
|
48
|
+
with no Transformers/PEFT. Driven by `tt pipeline run` from a foundation
|
|
49
49
|
`tunedtensor.json`. Pretrain, SFT, and RL require CUDA. Tokenizer training
|
|
50
50
|
and the CPU unit tests do not. The current runtime is single-process and
|
|
51
51
|
rejects `nproc_per_node` values above 1 and unsupported steps such as
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Local serving and coding harnesses
|
|
2
|
+
|
|
3
|
+
`tt serve` verifies TT artifacts and launches **vLLM 0.28.0**. vLLM owns model
|
|
4
|
+
execution, batching, KV/prefix caching, streaming, and native tool-call parsing.
|
|
5
|
+
There is no TT inference loop, HTTP server, or proxy between the harness and
|
|
6
|
+
vLLM. The serving dependencies are locked separately from training, in an
|
|
7
|
+
external cache environment; an npm upgrade does not mutate the training runtime.
|
|
8
|
+
|
|
9
|
+
## Requirements and scope
|
|
10
|
+
|
|
11
|
+
- The packaged serving runtime requires **Linux and NVIDIA CUDA**. CPU/macOS
|
|
12
|
+
serving is not supported by this runtime; CLI/package commands still work
|
|
13
|
+
there. Unsupported hardware or model/adapter combinations fail rather than
|
|
14
|
+
silently falling back to the old server.
|
|
15
|
+
- Install `uv` and prefetch the certified model with `tt models prefetch`.
|
|
16
|
+
First launch installs the locked serving dependencies and may compile kernels;
|
|
17
|
+
it needs substantially more time and disk than a warm request. CUDA JIT may
|
|
18
|
+
require a working CUDA toolkit/C++ toolchain. Model loading remains offline.
|
|
19
|
+
- Foundation checkpoints and image inputs are not supported by this path.
|
|
20
|
+
- Qwen3.5-2B base and a one-step, rank-16 `all-linear` TT-shaped adapter have been
|
|
21
|
+
exercised on GB10. The registry's Nemotron and Muse Glimmer architectures have
|
|
22
|
+
upstream support, but their exact adapters/hardware combinations are **not
|
|
23
|
+
certified by that Qwen smoke test**. No 70B model was tested here.
|
|
24
|
+
|
|
25
|
+
## Serve once, compare base and tuned
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
tt models prefetch tunedtensor.json
|
|
29
|
+
|
|
30
|
+
tt serve local-<run-id> --config local-runner.json \
|
|
31
|
+
--context-length 8192 --max-tokens 512
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This exposes **both** `local-<run-id>` and `base:<base-model-id>` at the same
|
|
35
|
+
endpoint, sharing the base weights. The adapter is registered at startup; TT
|
|
36
|
+
never enables runtime adapter upload/loading. There is no merge/export step
|
|
37
|
+
and no modification of the verified weights. `tt serve base` starts base-only.
|
|
38
|
+
`active` still fails when no adapter is activated.
|
|
39
|
+
|
|
40
|
+
Artifact integrity, certified base revisions, and stored behavior-prompt
|
|
41
|
+
fingerprints retain their verification. A stored adapter's behavior prompt is
|
|
42
|
+
applied to both model IDs for comparison. `tt serve base` without explicit
|
|
43
|
+
`--spec` does not inject the adjacent spec; use `--spec tunedtensor.json` for that.
|
|
44
|
+
`--no-spec-prompt` is the explicit opt-out. A small generated template prefix
|
|
45
|
+
preserves the owner instructions while leaving tool messages and upstream chat
|
|
46
|
+
formatting intact; it never changes the saved tokenizer/template.
|
|
47
|
+
|
|
48
|
+
### Resource controls
|
|
49
|
+
|
|
50
|
+
- `--context-length`: total context budget, default 16,384 tokens. Keep it large
|
|
51
|
+
enough for the harness's instructions and tool results, not just the question.
|
|
52
|
+
- `--max-tokens`: generation budget, default 512. vLLM receives this through its
|
|
53
|
+
generation configuration; the upstream API owns per-request validation.
|
|
54
|
+
- `--gpu-memory-utilization`: fraction of device memory budgeted to vLLM,
|
|
55
|
+
default 0.8. On shared-memory machines leave room for the OS and other work.
|
|
56
|
+
For the small Qwen GB10 smoke we used **0.15**; this is not suitable for every
|
|
57
|
+
model size. Reduce the context or memory budget when upstream reports a fit
|
|
58
|
+
failure, and ensure the weights themselves still fit.
|
|
59
|
+
- `--max-concurrent-requests`: maximum sequences scheduled in a vLLM batch,
|
|
60
|
+
default one, maximum eight. This is upstream batching, **not** the previous
|
|
61
|
+
HTTP admission/429 contract; upstream may queue additional requests.
|
|
62
|
+
|
|
63
|
+
Health becomes ready after loading/warm-up. Ctrl-C terminates the process group.
|
|
64
|
+
A port conflict is checked before loading; upstream owns the final bind and any
|
|
65
|
+
race. The unreleased custom server's `--merge-adapter` flag is removed.
|
|
66
|
+
|
|
67
|
+
## Pi
|
|
68
|
+
|
|
69
|
+
Export a new isolated configuration rather than replacing global settings:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
export PI_CODING_AGENT_DIR="$(mktemp -d)"
|
|
73
|
+
tt serve local-<run-id> --config local-runner.json \
|
|
74
|
+
--context-length 8192 --max-tokens 512 \
|
|
75
|
+
--print-client-config pi > "$PI_CODING_AGENT_DIR/models.json"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Use the same launch options as the running server. Exporting config verifies the
|
|
79
|
+
TT target but does not start a server. Adapter exports include both model IDs.
|
|
80
|
+
For example, in a disposable coding-fixture directory:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pi --provider tt-local --model 'base:Qwen/Qwen3.5-2B' \
|
|
84
|
+
--tools read --no-extensions --no-skills --no-prompt-templates \
|
|
85
|
+
--no-context-files --no-session --offline --thinking off \
|
|
86
|
+
-p 'Read fixture.py and explain what its function returns.'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Repeat with `--model local-<run-id>` in a fresh session. `--tools read` is a
|
|
90
|
+
read-only tool allowlist, **not a filesystem sandbox**. Use a disposable account
|
|
91
|
+
or an enforcing harness permission boundary for untrusted tasks. Enable edits
|
|
92
|
+
or shell commands only when you intend to grant those permissions.
|
|
93
|
+
|
|
94
|
+
For question-only comparisons, `--no-tools` remains an optional experiment
|
|
95
|
+
setting. It is no longer a server requirement. `reasoning: false` and
|
|
96
|
+
`supportsReasoningEffort: false` in the Pi export describe this endpoint's
|
|
97
|
+
reasoning-control contract; **neither disables tool calls or asserts that the
|
|
98
|
+
model cannot reason**.
|
|
99
|
+
|
|
100
|
+
## OpenCode
|
|
101
|
+
|
|
102
|
+
OpenCode can connect directly using its existing custom provider support:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"$schema": "https://opencode.ai/config.json",
|
|
107
|
+
"provider": {
|
|
108
|
+
"tt-local": {
|
|
109
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
110
|
+
"name": "TT local",
|
|
111
|
+
"options": { "baseURL": "http://127.0.0.1:8000/v1" },
|
|
112
|
+
"models": {
|
|
113
|
+
"base:Qwen/Qwen3.5-2B": {
|
|
114
|
+
"name": "TT base",
|
|
115
|
+
"limit": { "context": 8192, "output": 512 }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Add the exact adapter ID from `/v1/models` to compare it. Set the actual server
|
|
124
|
+
budgets, configure OpenCode's permissions, and select
|
|
125
|
+
`opencode run --model 'tt-local/base:Qwen/Qwen3.5-2B' ...`. TT does not install a
|
|
126
|
+
second agent framework or change global OpenCode configuration.
|
|
127
|
+
|
|
128
|
+
## Tool support and security
|
|
129
|
+
|
|
130
|
+
TT selects upstream parsers for the registered models:
|
|
131
|
+
|
|
132
|
+
- Qwen3.5-2B: `qwen3_xml`.
|
|
133
|
+
- Nemotron 3.5 Lightning: `qwen3_coder` (ordinary non-thinking template mode).
|
|
134
|
+
- Muse Glimmer: `muse_glimmer`.
|
|
135
|
+
|
|
136
|
+
These are protocol integrations, not guarantees that a model will choose the
|
|
137
|
+
right tool or produce correct code. A complete test must observe a tool call,
|
|
138
|
+
execute an allowed tool, send its result back, and verify the final answer.
|
|
139
|
+
The harness owns tool execution/permissions; vLLM only generates the calls.
|
|
140
|
+
Fine-tuning can change tool reliability, so compare the tuned model too.
|
|
141
|
+
|
|
142
|
+
The default bind is `127.0.0.1:8000`. Non-loopback binds require both
|
|
143
|
+
`--allow-remote` and `--api-key-env NAME`. An authentication-only middleware
|
|
144
|
+
extends the bearer check to **all** upstream HTTP routes, including health,
|
|
145
|
+
tokenization, and administrative routes; vLLM's built-in guard covers only
|
|
146
|
+
selected prefixes. No token is put in argv or exported client configuration.
|
|
147
|
+
Pi uses `${NAME}` interpolation; OpenCode can use `"apiKey": "{env:NAME}"`.
|
|
148
|
+
Provide the variable separately to each process. Without `--api-key-env`, local
|
|
149
|
+
serving is unauthenticated. Use a trusted host or encrypted tunnel, never expose
|
|
150
|
+
plain HTTP directly to the Internet. Request/output logging and vLLM usage
|
|
151
|
+
telemetry are disabled by the TT launcher.
|
|
152
|
+
|
|
153
|
+
## API and measurement
|
|
154
|
+
|
|
155
|
+
Use `/health`, `/v1/models`, and `/v1/chat/completions`. API bodies, SSE events,
|
|
156
|
+
usage, tool calls, cancellation, and error semantics come from the pinned
|
|
157
|
+
upstream server, not a TT approximation. The old custom server's message/byte
|
|
158
|
+
limits, HTTP errors, and health JSON are not preserved as an API contract.
|
|
159
|
+
|
|
160
|
+
Measure startup and first-request compilation separately from steady-state
|
|
161
|
+
first-text latency and tokens/second. Compare identical prompts, templates,
|
|
162
|
+
budgets, and fresh harness sessions on an otherwise-idle machine. Test generated
|
|
163
|
+
code independently; a tiny one-step adapter proves integration, not better
|
|
164
|
+
coding. Pi's zero provider token prices do not mean zero hardware cost.
|
|
165
|
+
|
|
166
|
+
References: [vLLM LoRA](https://docs.vllm.ai/en/v0.28.0/features/lora/),
|
|
167
|
+
[Nemotron tool configuration](https://docs.nvidia.com/nim/large-language-models/latest/get-started/advanced/get-started-nemotron-3.5-lightning.html),
|
|
168
|
+
[OpenCode custom providers](https://opencode.ai/docs/providers/#custom-provider).
|
|
@@ -108,7 +108,7 @@ tt doctor tunedtensor.json
|
|
|
108
108
|
tt validate tunedtensor.json
|
|
109
109
|
tt models prefetch tunedtensor.json
|
|
110
110
|
tt models verify-base tunedtensor.json
|
|
111
|
-
tt run tunedtensor.json
|
|
111
|
+
tt pipeline run --spec tunedtensor.json --config local-runner.json
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
`doctor` resolves the same bundled project and paths the run will use, imports
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuned-tensor/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Local TT agent and CUDA fine-tuning CLI for Tuned Tensor workflows",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
"training/adapter/pyproject.toml",
|
|
26
26
|
"training/adapter/uv.lock",
|
|
27
27
|
"training/adapter/src/*.py",
|
|
28
|
+
"training/serving/pyproject.toml",
|
|
29
|
+
"training/serving/uv.lock",
|
|
28
30
|
"training/foundation/pyproject.toml",
|
|
29
31
|
"training/foundation/uv.lock",
|
|
30
32
|
"training/foundation/src/*.py",
|
|
@@ -1,280 +1,146 @@
|
|
|
1
|
+
"""Artifact preparation only; vLLM owns HTTP, batching, streaming and tools."""
|
|
1
2
|
from __future__ import annotations
|
|
2
3
|
|
|
3
|
-
import hmac
|
|
4
4
|
import json
|
|
5
|
-
import
|
|
5
|
+
import hmac
|
|
6
6
|
import os
|
|
7
|
+
import signal
|
|
8
|
+
import socket
|
|
7
9
|
import sys
|
|
8
|
-
import threading
|
|
9
|
-
import time
|
|
10
|
-
import uuid
|
|
11
|
-
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
12
10
|
from pathlib import Path
|
|
13
11
|
from tempfile import TemporaryDirectory
|
|
14
|
-
from typing import Any
|
|
15
|
-
|
|
16
|
-
from evaluate import (
|
|
17
|
-
configure_hugging_face_cache,
|
|
18
|
-
import_runtime_dependencies,
|
|
19
|
-
load_text_model,
|
|
20
|
-
resolve_adapter_path,
|
|
21
|
-
sampling_kwargs,
|
|
22
|
-
)
|
|
23
|
-
from model_contract import assert_certified_base_model_revision, chat_template_kwargs
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
MODEL_ARTIFACT = os.environ.get("TT_MODEL_ARTIFACT")
|
|
27
|
-
BASE_MODEL = os.environ["TT_BASE_MODEL"]
|
|
28
|
-
MODEL_SOURCE = os.environ.get("TT_MODEL_SOURCE", BASE_MODEL)
|
|
29
|
-
BASE_MODEL_REVISION = os.environ.get("TT_BASE_MODEL_REVISION")
|
|
30
|
-
MODEL_NAME = os.environ.get("TT_MODEL_NAME", "tuned-tensor-local")
|
|
31
|
-
MODEL_LOADER = os.environ.get("TT_MODEL_LOADER", "causal_lm")
|
|
32
|
-
SYSTEM_PROMPT = os.environ.get("TT_SYSTEM_PROMPT", "").strip()
|
|
33
|
-
HOST = os.environ.get("TT_HOST", "127.0.0.1")
|
|
34
|
-
PORT = int(os.environ.get("TT_PORT", "8000"))
|
|
35
|
-
DEVICE_REQUEST = os.environ.get("TT_DEVICE", "cuda")
|
|
36
|
-
DEFAULT_MAX_TOKENS = int(os.environ.get("TT_MAX_TOKENS", "512"))
|
|
37
|
-
DEFAULT_TEMPERATURE = float(os.environ.get("TT_TEMPERATURE", "0"))
|
|
38
|
-
DEFAULT_TOP_P = float(os.environ.get("TT_TOP_P", "1"))
|
|
39
|
-
MAX_REQUEST_BYTES = 2 * 1024 * 1024
|
|
40
|
-
MAX_PROMPT_CHARS = 100_000
|
|
41
|
-
MAX_PROMPT_TOKENS = 16_384
|
|
42
|
-
MAX_MESSAGES = 128
|
|
43
|
-
API_KEY = os.environ.get("TT_API_KEY", "")
|
|
44
|
-
MAX_CONCURRENT_REQUESTS = int(os.environ.get("TT_MAX_CONCURRENT_REQUESTS", "1"))
|
|
45
|
-
|
|
46
|
-
|
|
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")
|
|
53
|
-
configure_hugging_face_cache(os.environ.get("HF_HOME"))
|
|
54
|
-
import_runtime_dependencies()
|
|
55
|
-
TEMP_DIR = TemporaryDirectory(prefix="tt-local-serve-")
|
|
56
|
-
ADAPTER_PATH = (
|
|
57
|
-
resolve_adapter_path(MODEL_ARTIFACT, Path(TEMP_DIR.name))
|
|
58
|
-
if MODEL_ARTIFACT
|
|
59
|
-
else None
|
|
60
|
-
)
|
|
61
|
-
MODEL_PAYLOAD = {
|
|
62
|
-
"base_model": BASE_MODEL,
|
|
63
|
-
"model_source": MODEL_SOURCE,
|
|
64
|
-
"base_model_revision": BASE_MODEL_REVISION,
|
|
65
|
-
"device": DEVICE_REQUEST,
|
|
66
|
-
"model_loader": MODEL_LOADER,
|
|
67
|
-
}
|
|
68
|
-
MODEL, TOKENIZER, DEVICE = load_text_model(MODEL_PAYLOAD, ADAPTER_PATH, MODEL_LOADER)
|
|
69
|
-
GENERATION_LOCK = threading.Lock()
|
|
70
|
-
REQUEST_SLOTS = threading.BoundedSemaphore(MAX_CONCURRENT_REQUESTS)
|
|
71
|
-
|
|
72
12
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
13
|
+
from evaluate import configure_hugging_face_cache, resolve_adapter_path
|
|
14
|
+
from model_contract import assert_certified_base_model_revision, assert_certified_model_config
|
|
15
|
+
|
|
16
|
+
class BearerAuthMiddleware:
|
|
17
|
+
"""Keep TT's auth boundary on ALL routes, including upstream admin APIs.
|
|
18
|
+
|
|
19
|
+
vLLM's default bearer guard only covers selected API prefixes. This is
|
|
20
|
+
transport admission only; requests/responses and generation stay upstream.
|
|
21
|
+
"""
|
|
22
|
+
def __init__(self, app):
|
|
23
|
+
self.app = app
|
|
24
|
+
self.expected = os.environ["TT_API_KEY"].encode()
|
|
25
|
+
|
|
26
|
+
async def __call__(self, scope, receive, send):
|
|
27
|
+
if scope["type"] in {"http", "websocket"}:
|
|
28
|
+
authorization = dict(scope.get("headers", [])).get(b"authorization", b"")
|
|
29
|
+
scheme, _, token = authorization.partition(b" ")
|
|
30
|
+
if scheme.lower() != b"bearer" or not hmac.compare_digest(token, self.expected):
|
|
31
|
+
if scope["type"] == "websocket":
|
|
32
|
+
await send({"type": "websocket.close", "code": 1008})
|
|
33
|
+
else:
|
|
34
|
+
await send({"type": "http.response.start", "status": 401,
|
|
35
|
+
"headers": [(b"content-type", b"application/json")]})
|
|
36
|
+
await send({"type": "http.response.body", "body": b'{"error":"Unauthorized"}'})
|
|
37
|
+
return
|
|
38
|
+
await self.app(scope, receive, send)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_vllm_args(model_source: str, adapter_path: str | None, temp_dir: Path) -> list[str]:
|
|
42
|
+
args = [
|
|
43
|
+
"serve", model_source,
|
|
44
|
+
"--served-model-name", os.environ["TT_MODEL_NAME"],
|
|
45
|
+
"--host", os.environ.get("TT_HOST", "127.0.0.1"),
|
|
46
|
+
"--port", os.environ.get("TT_PORT", "8000"),
|
|
47
|
+
"--max-model-len", os.environ.get("TT_CONTEXT_LENGTH", "16384"),
|
|
48
|
+
"--max-num-seqs", os.environ.get("TT_MAX_CONCURRENT_REQUESTS", "1"),
|
|
49
|
+
"--gpu-memory-utilization", os.environ.get("TT_GPU_MEMORY_UTILIZATION", "0.8"),
|
|
50
|
+
"--generation-config", "vllm",
|
|
51
|
+
"--override-generation-config", json.dumps({
|
|
52
|
+
"max_new_tokens": int(os.environ.get("TT_MAX_TOKENS", "512")),
|
|
53
|
+
"temperature": float(os.environ.get("TT_TEMPERATURE", "0")),
|
|
54
|
+
"top_p": float(os.environ.get("TT_TOP_P", "1")),
|
|
55
|
+
}),
|
|
56
|
+
"--language-model-only",
|
|
57
|
+
"--no-enable-log-requests", "--no-enable-log-outputs",
|
|
58
|
+
]
|
|
59
|
+
if os.environ.get("TT_API_KEY"):
|
|
60
|
+
args += ["--middleware", "serve.BearerAuthMiddleware"]
|
|
61
|
+
base = os.environ["TT_BASE_MODEL"]
|
|
62
|
+
# Upstream model-specific parsers, not a TT parser or model-size heuristic.
|
|
63
|
+
parser = {
|
|
64
|
+
"Qwen/Qwen3.5-2B": "qwen3_xml",
|
|
65
|
+
"nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16": "qwen3_coder",
|
|
66
|
+
"meta-models/Muse-Glimmer-30B": "muse_glimmer",
|
|
67
|
+
}[base]
|
|
68
|
+
args += ["--enable-auto-tool-choice", "--tool-call-parser", parser]
|
|
69
|
+
if base != "meta-models/Muse-Glimmer-30B":
|
|
70
|
+
args += ["--default-chat-template-kwargs", '{"enable_thinking":false}']
|
|
71
|
+
if adapter_path:
|
|
72
|
+
config = json.loads((Path(adapter_path) / "adapter_config.json").read_text())
|
|
73
|
+
adapter_base = config.get("base_model_name_or_path")
|
|
74
|
+
if adapter_base not in {os.environ["TT_BASE_MODEL"], model_source}:
|
|
75
|
+
raise ValueError("The adapter base does not match the verified serving model.")
|
|
76
|
+
base_name = "base:" + os.environ["TT_BASE_MODEL"]
|
|
77
|
+
args[args.index("--served-model-name") + 1] = base_name
|
|
78
|
+
args += ["--enable-lora", "--max-lora-rank", str(max(8, int(config["r"]))),
|
|
79
|
+
"--lora-modules", json.dumps({"name": os.environ["TT_MODEL_NAME"],
|
|
80
|
+
"path": adapter_path, "base_model_name": base_name})]
|
|
81
|
+
prompt = os.environ.get("TT_SYSTEM_PROMPT", "").strip()
|
|
82
|
+
if prompt:
|
|
83
|
+
source = Path(model_source)
|
|
84
|
+
template_file = source / "chat_template.jinja"
|
|
85
|
+
if template_file.is_file():
|
|
86
|
+
template = template_file.read_text(encoding="utf-8")
|
|
107
87
|
else:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
messages
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def bounded_number(value: Any, default: float, minimum: float, maximum: float) -> float:
|
|
156
|
-
number = float(default if value is None else value)
|
|
157
|
-
if not math.isfinite(number) or number < minimum or number > maximum:
|
|
158
|
-
raise ValueError(f"Generation value must be between {minimum} and {maximum}.")
|
|
159
|
-
return number
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def bounded_integer(value: Any, default: int, minimum: int, maximum: int) -> int:
|
|
163
|
-
number = bounded_number(value, default, minimum, maximum)
|
|
164
|
-
if not number.is_integer():
|
|
165
|
-
raise ValueError(f"Generation value must be an integer between {minimum} and {maximum}.")
|
|
166
|
-
return int(number)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
class Handler(BaseHTTPRequestHandler):
|
|
170
|
-
server_version = "TunedTensorLocalServer/0.1"
|
|
171
|
-
|
|
172
|
-
def setup(self) -> None:
|
|
173
|
-
super().setup()
|
|
174
|
-
self.connection.settimeout(30)
|
|
175
|
-
|
|
176
|
-
def authorized(self) -> bool:
|
|
177
|
-
if not API_KEY:
|
|
178
|
-
return True
|
|
179
|
-
supplied = self.headers.get("authorization", "")
|
|
180
|
-
return hmac.compare_digest(supplied, "Bearer " + API_KEY)
|
|
181
|
-
|
|
182
|
-
def send_json(self, status: int, payload: Any) -> None:
|
|
183
|
-
body = json.dumps(payload).encode("utf-8")
|
|
184
|
-
self.send_response(status)
|
|
185
|
-
self.send_header("Content-Type", "application/json")
|
|
186
|
-
self.send_header("Content-Length", str(len(body)))
|
|
187
|
-
self.end_headers()
|
|
188
|
-
self.wfile.write(body)
|
|
189
|
-
|
|
190
|
-
def do_GET(self) -> None: # noqa: N802
|
|
191
|
-
if not self.authorized():
|
|
192
|
-
self.send_json(401, {"error": {"message": "Unauthorized"}})
|
|
193
|
-
return
|
|
194
|
-
if self.path in {"/", "/health"}:
|
|
195
|
-
self.send_json(200, {"status": "ok", "model": MODEL_NAME, "device": DEVICE})
|
|
196
|
-
return
|
|
197
|
-
if self.path == "/v1/models":
|
|
198
|
-
self.send_json(200, {
|
|
199
|
-
"object": "list",
|
|
200
|
-
"data": [{"id": MODEL_NAME, "object": "model", "owned_by": "tuned-tensor-local"}],
|
|
201
|
-
})
|
|
202
|
-
return
|
|
203
|
-
self.send_json(404, {"error": {"message": "Not found"}})
|
|
204
|
-
|
|
205
|
-
def do_POST(self) -> None: # noqa: N802
|
|
206
|
-
if not self.authorized():
|
|
207
|
-
self.send_json(401, {"error": {"message": "Unauthorized"}})
|
|
208
|
-
return
|
|
209
|
-
if self.path not in {"/v1/chat/completions", "/chat/completions"}:
|
|
210
|
-
self.send_json(404, {"error": {"message": "Not found"}})
|
|
211
|
-
return
|
|
88
|
+
template = json.loads((source / "tokenizer_config.json").read_text())["chat_template"]
|
|
89
|
+
if not isinstance(template, str):
|
|
90
|
+
raise ValueError("Serving requires an unambiguous string chat template.")
|
|
91
|
+
# Literal data, never prompt text interpolated as Jinja source. Preserve
|
|
92
|
+
# tool calls/results verbatim while merging leading system context.
|
|
93
|
+
prefix = "{%- set tt = namespace(system=" + json.dumps(prompt) + ", history=[]) -%}"
|
|
94
|
+
prefix += "{%- for message in messages -%}{%- if message.role == 'system' -%}"
|
|
95
|
+
prefix += "{%- set tt.system = tt.system + '\\n\\n' + message.content -%}"
|
|
96
|
+
prefix += "{%- else -%}{%- set tt.history = tt.history + [message] -%}{%- endif -%}{%- endfor -%}"
|
|
97
|
+
prefix += "{%- set messages = [{'role': 'system', 'content': tt.system}] + tt.history -%}"
|
|
98
|
+
path = temp_dir / "chat-template.jinja"
|
|
99
|
+
path.write_text(prefix + template, encoding="utf-8")
|
|
100
|
+
args += ["--chat-template", str(path)]
|
|
101
|
+
return args
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def prepare_model_source() -> str:
|
|
105
|
+
base = os.environ["TT_BASE_MODEL"]
|
|
106
|
+
revision = os.environ.get("TT_BASE_MODEL_REVISION")
|
|
107
|
+
assert_certified_base_model_revision(base, revision, "Serving base model revision")
|
|
108
|
+
configure_hugging_face_cache(os.environ.get("HF_HOME"))
|
|
109
|
+
source = os.environ.get("TT_MODEL_SOURCE", base)
|
|
110
|
+
if not Path(source).is_dir():
|
|
111
|
+
if source != base:
|
|
112
|
+
raise ValueError("Explicit serving snapshot is not an existing directory.")
|
|
113
|
+
from huggingface_hub import snapshot_download
|
|
114
|
+
source = snapshot_download(repo_id=base, revision=revision, local_files_only=True)
|
|
115
|
+
config = json.loads((Path(source) / "config.json").read_text(encoding="utf-8"))
|
|
116
|
+
assert_certified_model_config(config, "Serving model config", base)
|
|
117
|
+
return str(Path(source).resolve())
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def run_server() -> None:
|
|
121
|
+
host = os.environ.get("TT_HOST", "127.0.0.1")
|
|
122
|
+
port = int(os.environ.get("TT_PORT", "8000"))
|
|
123
|
+
# Early diagnostic; upstream still owns the real bind and handles races.
|
|
124
|
+
family = socket.AF_INET6 if ":" in host else socket.AF_INET
|
|
125
|
+
with socket.socket(family, socket.SOCK_STREAM) as probe:
|
|
126
|
+
probe.bind((host, port))
|
|
127
|
+
model_source = prepare_model_source()
|
|
128
|
+
if os.environ.get("TT_API_KEY"):
|
|
129
|
+
os.environ["VLLM_API_KEY"] = os.environ["TT_API_KEY"]
|
|
130
|
+
with TemporaryDirectory(prefix="tt-serve-") as temp:
|
|
131
|
+
adapter = resolve_adapter_path(os.environ.get("TT_MODEL_ARTIFACT"), Path(temp))
|
|
132
|
+
args = build_vllm_args(model_source, adapter, Path(temp))
|
|
133
|
+
from vllm.entrypoints.cli.main import main
|
|
134
|
+
previous = sys.argv
|
|
212
135
|
try:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if not isinstance(body, dict):
|
|
218
|
-
raise ValueError("Request body must be a JSON object.")
|
|
219
|
-
if body.get("stream"):
|
|
220
|
-
self.send_json(400, {"error": {"message": "Streaming is not supported yet."}})
|
|
221
|
-
return
|
|
222
|
-
messages = normalize_messages(body.get("messages"))
|
|
223
|
-
generation = {
|
|
224
|
-
"max_new_tokens": bounded_integer(body.get("max_tokens"), DEFAULT_MAX_TOKENS, 1, 8192),
|
|
225
|
-
"temperature": bounded_number(body.get("temperature"), DEFAULT_TEMPERATURE, 0, 5),
|
|
226
|
-
"top_p": bounded_number(body.get("top_p"), DEFAULT_TOP_P, 0, 1),
|
|
227
|
-
}
|
|
228
|
-
if not REQUEST_SLOTS.acquire(blocking=False):
|
|
229
|
-
self.send_json(429, {"error": {"message": "The local model is busy; retry shortly."}})
|
|
230
|
-
return
|
|
231
|
-
started = time.perf_counter()
|
|
232
|
-
try:
|
|
233
|
-
content, prompt_tokens, completion_tokens = generate_text(messages, generation)
|
|
234
|
-
finally:
|
|
235
|
-
REQUEST_SLOTS.release()
|
|
236
|
-
latency_ms = round((time.perf_counter() - started) * 1000)
|
|
237
|
-
self.send_json(200, {
|
|
238
|
-
"id": "chatcmpl-" + uuid.uuid4().hex,
|
|
239
|
-
"object": "chat.completion",
|
|
240
|
-
"created": int(time.time()),
|
|
241
|
-
"model": MODEL_NAME,
|
|
242
|
-
"choices": [{
|
|
243
|
-
"index": 0,
|
|
244
|
-
"message": {"role": "assistant", "content": content},
|
|
245
|
-
"finish_reason": "stop",
|
|
246
|
-
}],
|
|
247
|
-
"usage": {
|
|
248
|
-
"prompt_tokens": prompt_tokens,
|
|
249
|
-
"completion_tokens": completion_tokens,
|
|
250
|
-
"total_tokens": prompt_tokens + completion_tokens,
|
|
251
|
-
},
|
|
252
|
-
"tt_local": {"latency_ms": latency_ms, "device": DEVICE},
|
|
253
|
-
})
|
|
254
|
-
except (ValueError, TypeError, json.JSONDecodeError) as exc:
|
|
255
|
-
self.send_json(400, {"error": {"message": str(exc)}})
|
|
256
|
-
except Exception as exc:
|
|
257
|
-
print(f"Model server error: {type(exc).__name__}: {exc}", file=sys.stderr, flush=True)
|
|
258
|
-
self.send_json(500, {"error": {"message": "Internal model server error."}})
|
|
259
|
-
|
|
260
|
-
def log_message(self, fmt: str, *args: Any) -> None:
|
|
261
|
-
print("%s - %s" % (self.address_string(), fmt % args), flush=True)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def main() -> None:
|
|
265
|
-
print(f"Serving {MODEL_NAME} on http://{HOST}:{PORT}", flush=True)
|
|
266
|
-
print(f"OpenAI-compatible endpoint: http://{HOST}:{PORT}/v1/chat/completions", flush=True)
|
|
267
|
-
print(f"Device: {DEVICE}", flush=True)
|
|
268
|
-
server = ThreadingHTTPServer((HOST, PORT), Handler)
|
|
269
|
-
server.daemon_threads = True
|
|
270
|
-
server.request_queue_size = max(2, MAX_CONCURRENT_REQUESTS * 2)
|
|
271
|
-
try:
|
|
272
|
-
server.serve_forever()
|
|
273
|
-
except KeyboardInterrupt:
|
|
274
|
-
print("Model server stopped.", flush=True)
|
|
275
|
-
finally:
|
|
276
|
-
server.server_close()
|
|
136
|
+
sys.argv = ["vllm", *args]
|
|
137
|
+
main()
|
|
138
|
+
finally:
|
|
139
|
+
sys.argv = previous
|
|
277
140
|
|
|
278
141
|
|
|
279
142
|
if __name__ == "__main__":
|
|
280
|
-
|
|
143
|
+
# Ensure temporary artifacts unwind if stopped before upstream installs its
|
|
144
|
+
# own graceful shutdown handlers. Node forwards signals to the whole group.
|
|
145
|
+
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
|
|
146
|
+
run_server()
|