blitz-cli 0.5.0__tar.gz → 0.6.0__tar.gz
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.
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/PKG-INFO +1 -1
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/__init__.py +1 -1
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_scaffold.py +33 -3
- blitz_cli-0.6.0/blitz_cli/_tool_schemas.py +189 -0
- blitz_cli-0.6.0/blitz_cli/templates/codeassist_Dockerfile.train.tmpl +26 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/codeassist_Makefile.tmpl +1 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/codeassist_README.md.tmpl +4 -1
- blitz_cli-0.6.0/blitz_cli/templates/codeassist_requirements_train.txt.tmpl +17 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/codeassist_train.py.tmpl +79 -11
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/pyproject.toml +1 -1
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_scaffold.py +87 -4
- blitz_cli-0.6.0/tests/test_tool_schemas.py +117 -0
- blitz_cli-0.5.0/blitz_cli/templates/codeassist_Dockerfile.train.tmpl +0 -17
- blitz_cli-0.5.0/blitz_cli/templates/codeassist_requirements_train.txt.tmpl +0 -15
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/.github/workflows/homebrew.yml +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/.github/workflows/publish.yml +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/.gitignore +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/LICENSE +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/README.md +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_activity.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_client.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_codeassist.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_dataset.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_local.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_make.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_redact.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_release.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_scan/__init__.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_scan/_classify.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_scan/_js.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_scan/_models.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_scan/_python.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_scan/_report.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_scan/_walk.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_skill.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_sync.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_toml.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_traces.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/cli.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/skills/__init__.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/skills/blitz-fix-teacher/SKILL.md +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/skills/blitz-instrument/SKILL.md +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/skills/blitz-setup-training/SKILL.md +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/skills/blitz-simulate-traces/SKILL.md +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/Dockerfile.tmpl +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/Makefile.tmpl +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/README.md.tmpl +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/__init__.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/codeassist_Dockerfile.tmpl +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/codeassist_dockerignore.tmpl +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/codeassist_requirements.txt.tmpl +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/dockerignore.tmpl +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/requirements.txt.tmpl +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/serve.py.tmpl +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/train.py.tmpl +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/watch.py.tmpl +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_activity.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_cli.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_codeassist.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_local.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_make.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_redact.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_release.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_scan.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_serve.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_sync.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_toml.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_traces.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blitz-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Developer CLI for Blitz: local-first tracing/init, codebase instrumentation scan, and scaffolds for both a QLoRA workflow trainer and a coding-agent session-to-fine-tuning-corpus watcher.
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -23,8 +23,17 @@ from string import Template
|
|
|
23
23
|
|
|
24
24
|
def clamp_seq_len(raw: int) -> int:
|
|
25
25
|
"""Clamp a raw sequence length (p95_input + max_output) to the trainer's
|
|
26
|
-
supported range. Defaults to 2048 when nothing usable is supplied.
|
|
27
|
-
|
|
26
|
+
supported range. Defaults to 2048 when nothing usable is supplied.
|
|
27
|
+
|
|
28
|
+
Upper bound is 16384, not 8192 -- measured (not estimated) against a
|
|
29
|
+
real code-assist corpus + the real Qwen tokenizer with a full
|
|
30
|
+
tool-schema block included on every example: at 8192 the schema tax
|
|
31
|
+
alone costs +4.4pp over-budget turns versus its own no-schema
|
|
32
|
+
baseline, but at 16384 that gap shrinks to +0.8pp (nearly free
|
|
33
|
+
relative to baseline at that length). This only permits more headroom
|
|
34
|
+
for callers who ask for it -- any caller requesting <=8192 behaves
|
|
35
|
+
identically to before."""
|
|
36
|
+
return max(1024, min(16384, int(raw or 0) or 2048))
|
|
28
37
|
|
|
29
38
|
|
|
30
39
|
def build_config(
|
|
@@ -66,6 +75,27 @@ def build_config(
|
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
|
|
78
|
+
def apply_batch_override(cfg: dict, per_device_batch_size: int) -> dict:
|
|
79
|
+
"""Override just the per-device batch size from build_config's
|
|
80
|
+
params_b-driven bucket, independent of LoRA rank.
|
|
81
|
+
|
|
82
|
+
``params_b`` ties batch size and LoRA rank together as one bucket, but
|
|
83
|
+
they hit different memory limits (batch size scales per-step activation/
|
|
84
|
+
logits memory; LoRA rank scales adapter memory) -- a card that's tight on
|
|
85
|
+
one but fine on the other has no way to ask for just one via ``params_b``
|
|
86
|
+
alone. Rescales ``grad_accum`` to hold the effective batch size
|
|
87
|
+
(``per_device_batch_size * grad_accum``) steady, so unlike ``seq_len``
|
|
88
|
+
this has no training-dynamics tradeoff: it only trades step count for
|
|
89
|
+
peak memory.
|
|
90
|
+
"""
|
|
91
|
+
effective_batch = cfg["per_device_batch_size"] * cfg["grad_accum"]
|
|
92
|
+
return {
|
|
93
|
+
**cfg,
|
|
94
|
+
"per_device_batch_size": per_device_batch_size,
|
|
95
|
+
"grad_accum": max(1, round(effective_batch / per_device_batch_size)),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
69
99
|
def _template(name: str) -> str:
|
|
70
100
|
return resources.files("blitz_cli.templates").joinpath(name).read_text(
|
|
71
101
|
encoding="utf-8"
|
|
@@ -109,7 +139,7 @@ def render(out_dir: Path, workflow: str) -> None:
|
|
|
109
139
|
_write(out_dir / name, Template(_template(name + ".tmpl")).safe_substitute(subs))
|
|
110
140
|
|
|
111
141
|
|
|
112
|
-
_DEFAULT_CODE_ASSIST_BASE_MODEL = "Qwen/Qwen2.5-Coder-
|
|
142
|
+
_DEFAULT_CODE_ASSIST_BASE_MODEL = "Qwen/Qwen2.5-Coder-7B-Instruct"
|
|
113
143
|
|
|
114
144
|
|
|
115
145
|
def render_code_assist(
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Formal JSON-Schema tool definitions for training-time `tools=` inclusion
|
|
2
|
+
(see `codeassist_train.py.tmpl`'s `to_text()`).
|
|
3
|
+
|
|
4
|
+
Without this, the model never sees a formal tool schema during training —
|
|
5
|
+
it only ever learns bare observed `(name, arguments)` pairs shaped like
|
|
6
|
+
this environment's own tool vocabulary. That risks teaching narrow
|
|
7
|
+
memorization of one fixed vocabulary rather than the general "read a given
|
|
8
|
+
schema, comply with it" skill that lets a model like Claude drive arbitrary
|
|
9
|
+
harnesses (a harness sends its own tool schemas at inference time; the
|
|
10
|
+
model reads whatever it's handed). Training *with* `tools=` included is
|
|
11
|
+
meant to teach that general skill instead.
|
|
12
|
+
|
|
13
|
+
Field shapes here are sampled directly from real `tool_input` values in
|
|
14
|
+
this environment's own corpus (`data/activity.db`) and from
|
|
15
|
+
`_activity.py`'s own summarizer/extractor functions for the 11 vanilla
|
|
16
|
+
Claude Code tools -- not guessed. `required` is a schema-design judgment
|
|
17
|
+
call layered on top in every case: `_activity.py` itself never enforces
|
|
18
|
+
required-ness (every reader is a lenient `.get()`), and the real captured
|
|
19
|
+
data is best-effort, not a formal spec pulled from documentation. Treat
|
|
20
|
+
this the same way as `_redact.py`: best-effort coverage of the tools this
|
|
21
|
+
environment is actually observed calling, not a guaranteed-complete or
|
|
22
|
+
guaranteed-accurate schema for any of them.
|
|
23
|
+
|
|
24
|
+
**Known, explicit gap**: this module only covers tools actually observed
|
|
25
|
+
in this environment's corpus. A tool_call to anything outside
|
|
26
|
+
`TOOL_SCHEMAS` means the training example gets rendered with `tools=None`
|
|
27
|
+
entirely (see `to_text()`'s uncovered-tool guard) rather than a `tools=`
|
|
28
|
+
list that's missing what the assistant then calls -- omitting a tool from
|
|
29
|
+
the shown schema while still training the model to call it is the exact
|
|
30
|
+
anti-pattern schema-conditioning exists to prevent.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _fn(name: str, description: str, properties: dict, required: list) -> dict:
|
|
37
|
+
return {
|
|
38
|
+
"type": "function",
|
|
39
|
+
"function": {
|
|
40
|
+
"name": name,
|
|
41
|
+
"description": description,
|
|
42
|
+
"parameters": {"type": "object", "properties": properties, "required": required},
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_STR = {"type": "string"}
|
|
48
|
+
_INT = {"type": "integer"}
|
|
49
|
+
_BOOL = {"type": "boolean"}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _arr(item_type) -> dict:
|
|
53
|
+
return {"type": "array", "items": item_type}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
TOOL_SCHEMAS: dict = {
|
|
57
|
+
# -- 11 vanilla Claude Code tools (fields from _activity.py's own
|
|
58
|
+
# _summarize_*/_full_* extractors) --
|
|
59
|
+
"Read": _fn(
|
|
60
|
+
"Read", "Read a file.",
|
|
61
|
+
{"file_path": _STR, "offset": _INT, "limit": _INT},
|
|
62
|
+
["file_path"],
|
|
63
|
+
),
|
|
64
|
+
"Edit": _fn(
|
|
65
|
+
"Edit", "Edit a file via exact string replacement.",
|
|
66
|
+
{"file_path": _STR, "old_string": _STR, "new_string": _STR, "replace_all": _BOOL},
|
|
67
|
+
["file_path", "old_string", "new_string"],
|
|
68
|
+
),
|
|
69
|
+
"Write": _fn(
|
|
70
|
+
"Write", "Write a file, creating or overwriting it.",
|
|
71
|
+
{"file_path": _STR, "content": _STR},
|
|
72
|
+
["file_path", "content"],
|
|
73
|
+
),
|
|
74
|
+
"Bash": _fn(
|
|
75
|
+
"Bash", "Run a shell command.",
|
|
76
|
+
{"command": _STR, "timeout": _INT, "description": _STR, "run_in_background": _BOOL},
|
|
77
|
+
["command"],
|
|
78
|
+
),
|
|
79
|
+
"Grep": _fn(
|
|
80
|
+
"Grep", "Search file contents (ripgrep-backed).",
|
|
81
|
+
{
|
|
82
|
+
"pattern": _STR, "path": _STR, "-i": _BOOL, "-n": _BOOL,
|
|
83
|
+
"-A": _INT, "-B": _INT, "-C": _INT, "output_mode": _STR,
|
|
84
|
+
"type": _STR, "head_limit": _INT, "multiline": _BOOL,
|
|
85
|
+
},
|
|
86
|
+
["pattern"],
|
|
87
|
+
),
|
|
88
|
+
"Glob": _fn(
|
|
89
|
+
"Glob", "Find files by glob pattern.",
|
|
90
|
+
{"pattern": _STR, "path": _STR},
|
|
91
|
+
["pattern"],
|
|
92
|
+
),
|
|
93
|
+
"Agent": _fn(
|
|
94
|
+
"Agent", "Dispatch a sub-agent task.",
|
|
95
|
+
{"description": _STR, "prompt": _STR, "subagent_type": _STR},
|
|
96
|
+
["description", "prompt", "subagent_type"],
|
|
97
|
+
),
|
|
98
|
+
"TodoWrite": _fn(
|
|
99
|
+
"TodoWrite", "Replace the full todo list.",
|
|
100
|
+
{"todos": _arr({"type": "object", "properties": {
|
|
101
|
+
"content": _STR, "status": _STR, "activeForm": _STR,
|
|
102
|
+
}})},
|
|
103
|
+
["todos"],
|
|
104
|
+
),
|
|
105
|
+
"WebSearch": _fn(
|
|
106
|
+
"WebSearch", "Search the web.",
|
|
107
|
+
{"query": _STR, "allowed_domains": _arr(_STR), "blocked_domains": _arr(_STR)},
|
|
108
|
+
["query"],
|
|
109
|
+
),
|
|
110
|
+
"WebFetch": _fn(
|
|
111
|
+
"WebFetch", "Fetch a URL and extract content per a prompt.",
|
|
112
|
+
{"url": _STR, "prompt": _STR},
|
|
113
|
+
["url", "prompt"],
|
|
114
|
+
),
|
|
115
|
+
"AskUserQuestion": _fn(
|
|
116
|
+
"AskUserQuestion", "Ask the user a structured multiple-choice question.",
|
|
117
|
+
{"questions": _arr({"type": "object", "properties": {
|
|
118
|
+
"question": _STR, "header": _STR, "multiSelect": _BOOL,
|
|
119
|
+
"options": _arr({"type": "object", "properties": {
|
|
120
|
+
"label": _STR, "description": _STR,
|
|
121
|
+
}}),
|
|
122
|
+
}})},
|
|
123
|
+
["questions"],
|
|
124
|
+
),
|
|
125
|
+
# -- 13 harness-native tools, sampled from real tool_input values in
|
|
126
|
+
# this environment's own corpus (data/activity.db) --
|
|
127
|
+
"DesignSync": _fn(
|
|
128
|
+
"DesignSync", "Sync a design doc.",
|
|
129
|
+
{"method": _STR, "projectId": _STR},
|
|
130
|
+
["method", "projectId"],
|
|
131
|
+
),
|
|
132
|
+
"EnterPlanMode": _fn("EnterPlanMode", "Enter plan mode.", {}, []),
|
|
133
|
+
"ExitPlanMode": _fn(
|
|
134
|
+
"ExitPlanMode", "Exit plan mode with a plan for approval.",
|
|
135
|
+
{"plan": _STR},
|
|
136
|
+
["plan"],
|
|
137
|
+
),
|
|
138
|
+
"Monitor": _fn(
|
|
139
|
+
"Monitor", "Monitor a long-running command, streaming matching lines as events.",
|
|
140
|
+
{"description": _STR, "command": _STR, "timeout_ms": _INT, "persistent": _BOOL},
|
|
141
|
+
["description", "command"],
|
|
142
|
+
),
|
|
143
|
+
"ScheduleWakeup": _fn(
|
|
144
|
+
"ScheduleWakeup", "Schedule a future wakeup.",
|
|
145
|
+
{"delaySeconds": _INT, "reason": _STR, "prompt": _STR},
|
|
146
|
+
["delaySeconds", "reason", "prompt"],
|
|
147
|
+
),
|
|
148
|
+
"SendMessage": _fn(
|
|
149
|
+
"SendMessage", "Send a message to another agent.",
|
|
150
|
+
{"to": _STR, "message": _STR, "summary": _STR},
|
|
151
|
+
["to", "message"],
|
|
152
|
+
),
|
|
153
|
+
"Skill": _fn(
|
|
154
|
+
"Skill", "Invoke a named skill.",
|
|
155
|
+
{"skill": _STR, "args": _STR},
|
|
156
|
+
["skill"],
|
|
157
|
+
),
|
|
158
|
+
"TaskCreate": _fn(
|
|
159
|
+
"TaskCreate", "Create a tracked background task.",
|
|
160
|
+
{"subject": _STR, "description": _STR},
|
|
161
|
+
["subject"],
|
|
162
|
+
),
|
|
163
|
+
"TaskList": _fn("TaskList", "List tracked tasks.", {}, []),
|
|
164
|
+
"TaskOutput": _fn(
|
|
165
|
+
"TaskOutput", "Get a tracked task's output.",
|
|
166
|
+
{"task_id": _STR, "block": _BOOL, "timeout": _INT},
|
|
167
|
+
["task_id"],
|
|
168
|
+
),
|
|
169
|
+
"TaskStop": _fn(
|
|
170
|
+
"TaskStop", "Stop a running tracked task.",
|
|
171
|
+
{"task_id": _STR},
|
|
172
|
+
["task_id"],
|
|
173
|
+
),
|
|
174
|
+
"TaskUpdate": _fn(
|
|
175
|
+
"TaskUpdate", "Update a tracked task's status.",
|
|
176
|
+
# NB: `taskId`, not `task_id` -- a real inconsistency observed in
|
|
177
|
+
# the corpus versus TaskOutput/TaskStop, kept faithfully rather
|
|
178
|
+
# than silently normalized.
|
|
179
|
+
{"taskId": _STR, "status": _STR},
|
|
180
|
+
["taskId", "status"],
|
|
181
|
+
),
|
|
182
|
+
"ToolSearch": _fn(
|
|
183
|
+
"ToolSearch", "Search for a deferred tool by name or keyword.",
|
|
184
|
+
{"query": _STR, "max_results": _INT},
|
|
185
|
+
["query"],
|
|
186
|
+
),
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
ALL_TOOLS: list = sorted(TOOL_SCHEMAS.values(), key=lambda t: t["function"]["name"])
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Heavy QLoRA trainer for the code-assist corpus (data/activity.db). Kept as
|
|
2
|
+
# a SEPARATE image from the lightweight watcher (Dockerfile) -- the always-on
|
|
3
|
+
# watcher just polls the filesystem and doesn't need a GPU or the ML stack;
|
|
4
|
+
# baking that stack into the same image it runs continuously would bloat a
|
|
5
|
+
# daemon meant to idle for weeks. This image is only built/run on demand for
|
|
6
|
+
# `blitz code-assist train`.
|
|
7
|
+
#
|
|
8
|
+
# CUDA 13.0, not 12.x: needed for Blackwell (sm_120, e.g. RTX 50-series)
|
|
9
|
+
# kernels -- torch/bitsandbytes built against older CUDA raise "no kernel
|
|
10
|
+
# image is available for execution on the device" on those cards. Verified
|
|
11
|
+
# against a real RTX 5070 Ti (driver reports CUDA 13.0 as its max supported
|
|
12
|
+
# version, so this matches rather than exceeds the host).
|
|
13
|
+
FROM pytorch/pytorch:2.13.0-cuda13.0-cudnn9-runtime
|
|
14
|
+
|
|
15
|
+
WORKDIR /workspace
|
|
16
|
+
ENV PYTHONUNBUFFERED=1 HF_HOME=/workspace/.hf
|
|
17
|
+
|
|
18
|
+
COPY requirements-train.txt .
|
|
19
|
+
# --break-system-packages: this base image ships a PEP 668 "externally
|
|
20
|
+
# managed" system Python (unlike the old conda-based 2.5.1 image), and there's
|
|
21
|
+
# no venv to install into here -- it's a single-purpose training container.
|
|
22
|
+
RUN pip install --no-cache-dir --break-system-packages -r requirements-train.txt
|
|
23
|
+
|
|
24
|
+
COPY train.py .
|
|
25
|
+
|
|
26
|
+
CMD ["python", "train.py"]
|
|
@@ -43,6 +43,7 @@ RUN = docker run -d \
|
|
|
43
43
|
TRAIN_RUN = docker run --rm --gpus all \
|
|
44
44
|
-v $(CURDIR):/workspace \
|
|
45
45
|
-e BASE_MODEL_HF=$(BASE_MODEL_HF) \
|
|
46
|
+
-e PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \
|
|
46
47
|
$(TRAIN_IMAGE)
|
|
47
48
|
|
|
48
49
|
.PHONY: build run logs stop shell build-train train
|
|
@@ -79,7 +79,10 @@ are already chat-formatted, tool-calling trajectories. Override the base
|
|
|
79
79
|
model per-run with `BASE_MODEL_HF=<hf-id>`, or bake in a different default at
|
|
80
80
|
scaffold time with `blitz scaffold code-assist --base-model <hf-id>`. Other
|
|
81
81
|
overrides: `SEQ_LEN`, `PARAMS_B` (drives LoRA rank/batch-size defaults),
|
|
82
|
-
`EVAL_FRACTION
|
|
82
|
+
`EVAL_FRACTION`, and `PER_DEVICE_BATCH_SIZE` (overrides just the batch size
|
|
83
|
+
from `PARAMS_B`'s bucket, independent of LoRA rank -- rescales `grad_accum`
|
|
84
|
+
to hold the effective batch size steady, so it's a free memory/step-count
|
|
85
|
+
tradeoff with no effect on training dynamics, unlike `SEQ_LEN`).
|
|
83
86
|
|
|
84
87
|
Each run is tracked in `training.db` (MLflow's SQLite backend) and saves its
|
|
85
88
|
adapter to `adapter-v<N>` (N = that run's registered model version), so
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# QLoRA SFT stack for training on the code-assist corpus (data/activity.db).
|
|
2
|
+
# torch is intentionally omitted -- the Dockerfile.train's pytorch/cuda base
|
|
3
|
+
# image ships the matching CUDA build, and reinstalling risks pulling a
|
|
4
|
+
# CPU-only wheel or one built for the wrong CUDA/arch.
|
|
5
|
+
transformers==4.48.0
|
|
6
|
+
trl==0.13.0
|
|
7
|
+
peft==0.14.0
|
|
8
|
+
accelerate==1.2.1
|
|
9
|
+
# 0.45.0 has no compiled kernels for Blackwell (sm_120, e.g. RTX 50-series) --
|
|
10
|
+
# 4-bit quantization raises "no kernel image is available for execution on
|
|
11
|
+
# the device" on those cards. 0.49.2 verified working on a real RTX 5070 Ti.
|
|
12
|
+
bitsandbytes==0.49.2
|
|
13
|
+
datasets==3.2.0
|
|
14
|
+
sentencepiece==0.2.0
|
|
15
|
+
# Local SQLite tracking/registry for this run (sqlite:///training.db) -- no
|
|
16
|
+
# `mlflow server` process, just the tracking client + SQLAlchemy backend.
|
|
17
|
+
mlflow==2.19.0
|
|
@@ -6,10 +6,36 @@ Defaults to Qwen2.5-Coder-14B-Instruct: our training examples are already
|
|
|
6
6
|
chat-formatted, tool-calling trajectories (native Claude Code tool names --
|
|
7
7
|
Read/Edit/Write/Bash/...), and an instruction-tuned checkpoint already
|
|
8
8
|
understands a chat template and basic tool-calling behavior, which is the
|
|
9
|
-
right starting point for further SFT rather than a raw base model.
|
|
10
|
-
|
|
9
|
+
right starting point for further SFT rather than a raw base model. 14B
|
|
10
|
+
assumes real headroom (a rented 80GB-class GPU) -- on a 16GB-class consumer
|
|
11
|
+
card, 7B was the safe default instead (14B OOM'd during LoRA adapter setup,
|
|
12
|
+
before any forward/backward pass, on a real RTX 5070 Ti). Override the base
|
|
13
|
+
model via the BASE_MODEL_HF env var (also settable at scaffold time with
|
|
11
14
|
`blitz scaffold code-assist --base-model <hf-id>`, which bakes a new default
|
|
12
|
-
into this project's Makefile)
|
|
15
|
+
into this project's Makefile) -- and PARAMS_B to match, since it drives the
|
|
16
|
+
LoRA rank/batch-size defaults in _scaffold.build_config.
|
|
17
|
+
|
|
18
|
+
SEQ_LEN defaults to 16384, not 2048, once real headroom is available --
|
|
19
|
+
measured (not estimated) against the real corpus + the real Qwen tokenizer
|
|
20
|
+
with the full tool-schema block from _tool_schemas.py included on every
|
|
21
|
+
example (see below): at seq_len=2048 the schema tax alone pushes 77.6% of
|
|
22
|
+
turns over budget (vs. 42.5% with no schema at all -- a +35.1pp hit); at
|
|
23
|
+
16384 that gap shrinks to +0.8pp (7.3% vs. 6.5%), i.e. the schema is nearly
|
|
24
|
+
free relative to its own baseline at this length. On a memory-constrained
|
|
25
|
+
card, packing still splits a turn longer than SEQ_LEN across separate
|
|
26
|
+
training steps with no attention connection between the pieces -- raise or
|
|
27
|
+
lower this based on actual available VRAM, using the measured table above
|
|
28
|
+
as a guide rather than guessing.
|
|
29
|
+
|
|
30
|
+
If you're still tight on memory after that: PER_DEVICE_BATCH_SIZE overrides
|
|
31
|
+
build_config's params_b-driven batch size directly, independent of LoRA
|
|
32
|
+
rank (unlike PARAMS_B, which moves both together as one bucket). Lowering
|
|
33
|
+
it rescales grad_accum to hold the effective batch size steady, so --
|
|
34
|
+
unlike SEQ_LEN -- this one is free: no training-dynamics or data-quality
|
|
35
|
+
tradeoff, just more (smaller) steps for the same total memory footprint per
|
|
36
|
+
step. On real hardware with unmeasured headroom, start with a conservative
|
|
37
|
+
guess and watch `nvidia-smi` on the first few steps rather than trusting a
|
|
38
|
+
guessed number.
|
|
13
39
|
|
|
14
40
|
Also tracks the run in ./training.db (MLflow's SQLite backend -- no `mlflow
|
|
15
41
|
server` process, just a local file): per-step loss history and a registered
|
|
@@ -33,15 +59,17 @@ from peft import LoraConfig, prepare_model_for_kbit_training
|
|
|
33
59
|
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
|
|
34
60
|
from trl import SFTConfig, SFTTrainer
|
|
35
61
|
|
|
36
|
-
from blitz_cli import _dataset, _scaffold
|
|
62
|
+
from blitz_cli import _dataset, _scaffold, _tool_schemas
|
|
37
63
|
|
|
38
64
|
mlflow.set_tracking_uri("sqlite:///training.db")
|
|
39
65
|
|
|
40
66
|
_BASE_MODEL_HF = os.environ.get("BASE_MODEL_HF", "Qwen/Qwen2.5-Coder-14B-Instruct")
|
|
41
67
|
_PARAMS_B = float(os.environ.get("PARAMS_B", "14"))
|
|
42
|
-
_SEQ_LEN = _scaffold.clamp_seq_len(int(os.environ.get("SEQ_LEN", "
|
|
68
|
+
_SEQ_LEN = _scaffold.clamp_seq_len(int(os.environ.get("SEQ_LEN", "16384")))
|
|
43
69
|
_EVAL_FRACTION = float(os.environ.get("EVAL_FRACTION", "0.1"))
|
|
44
70
|
_DB_PATH = os.environ.get("ACTIVITY_DB_PATH", "data/activity.db")
|
|
71
|
+
_SAVE_STEPS = int(os.environ.get("SAVE_STEPS", "50"))
|
|
72
|
+
_ALL_TOOL_NAMES = frozenset(_tool_schemas.TOOL_SCHEMAS)
|
|
45
73
|
|
|
46
74
|
cfg = _scaffold.build_config(
|
|
47
75
|
workflow="code-assist",
|
|
@@ -50,7 +78,15 @@ cfg = _scaffold.build_config(
|
|
|
50
78
|
seq_len=_SEQ_LEN,
|
|
51
79
|
eval_fraction=_EVAL_FRACTION,
|
|
52
80
|
)
|
|
53
|
-
|
|
81
|
+
|
|
82
|
+
_batch_override = os.environ.get("PER_DEVICE_BATCH_SIZE")
|
|
83
|
+
if _batch_override:
|
|
84
|
+
cfg = _scaffold.apply_batch_override(cfg, int(_batch_override))
|
|
85
|
+
|
|
86
|
+
print(
|
|
87
|
+
f"Base model: {cfg['base_model_hf']} seq_len={cfg['seq_len']} params_b={cfg['params_b']} "
|
|
88
|
+
f"per_device_batch_size={cfg['per_device_batch_size']} grad_accum={cfg['grad_accum']}"
|
|
89
|
+
)
|
|
54
90
|
|
|
55
91
|
if not Path(_DB_PATH).exists():
|
|
56
92
|
raise SystemExit(
|
|
@@ -123,10 +159,36 @@ def to_text(example):
|
|
|
123
159
|
# the model's own chat template. Assistant messages carry `tool_calls`;
|
|
124
160
|
# tool-role messages carry `tool_call_id`. If tool-call rendering doesn't
|
|
125
161
|
# look right for the exact Qwen revision in use, check its chat template
|
|
126
|
-
# in tokenizer_config.json
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
|
|
162
|
+
# in tokenizer_config.json.
|
|
163
|
+
#
|
|
164
|
+
# `arguments` is stored as a JSON *string* (the real OpenAI wire format --
|
|
165
|
+
# function.arguments is a string there too), but Qwen's chat template
|
|
166
|
+
# does `tool_call.arguments | tojson`, which assumes a raw object and
|
|
167
|
+
# JSON-encodes it itself. Feeding it an already-stringified value double-
|
|
168
|
+
# encodes it (confirmed via a real corpus example: `"arguments":
|
|
169
|
+
# "{\"file_path\": ...}"` instead of a clean nested object), producing
|
|
170
|
+
# garbled training text and abnormally high loss. Parse it back to a
|
|
171
|
+
# dict here, at render time only -- the stored messages_json keeps the
|
|
172
|
+
# portable OpenAI-string convention for any other consumer.
|
|
173
|
+
#
|
|
174
|
+
# tools=: pass the full fixed tool-schema list (_tool_schemas.ALL_TOOLS)
|
|
175
|
+
# on every example, so the model learns to read a given schema and
|
|
176
|
+
# comply with it -- not just memorize this environment's fixed tool
|
|
177
|
+
# vocabulary. If an example calls a tool outside _tool_schemas.TOOL_SCHEMAS,
|
|
178
|
+
# fall back to tools=None entirely rather than show a list that's
|
|
179
|
+
# missing what the assistant then calls -- that mismatch is worse than
|
|
180
|
+
# showing no schema at all.
|
|
181
|
+
messages = json.loads(json.dumps(example["messages"])) # deep copy, don't mutate the dataset row
|
|
182
|
+
tool_names = set()
|
|
183
|
+
for m in messages:
|
|
184
|
+
for tc in m.get("tool_calls") or []:
|
|
185
|
+
fn = tc.get("function") or {}
|
|
186
|
+
if fn.get("name"):
|
|
187
|
+
tool_names.add(fn["name"])
|
|
188
|
+
if isinstance(fn.get("arguments"), str):
|
|
189
|
+
fn["arguments"] = json.loads(fn["arguments"])
|
|
190
|
+
tools = _tool_schemas.ALL_TOOLS if tool_names <= _ALL_TOOL_NAMES else None
|
|
191
|
+
return {"text": tokenizer.apply_chat_template(messages, tools=tools, tokenize=False)}
|
|
130
192
|
|
|
131
193
|
|
|
132
194
|
dataset = dataset.map(to_text, remove_columns=dataset.column_names)
|
|
@@ -141,7 +203,13 @@ sft_config = SFTConfig(
|
|
|
141
203
|
packing=True,
|
|
142
204
|
bf16=True,
|
|
143
205
|
logging_steps=10,
|
|
144
|
-
|
|
206
|
+
# steps, not epoch: LoRA checkpoints are small and cheap to write, so
|
|
207
|
+
# there's no real cost to checkpointing often -- only upside if a run
|
|
208
|
+
# gets interrupted (a spot instance reclaim, a crash, anything) partway
|
|
209
|
+
# through what could be an hours-long epoch on a real corpus. Tune
|
|
210
|
+
# SAVE_STEPS once real steps/sec is known for a ~5-10 minute cadence.
|
|
211
|
+
save_strategy="steps",
|
|
212
|
+
save_steps=_SAVE_STEPS,
|
|
145
213
|
lr_scheduler_type="cosine",
|
|
146
214
|
warmup_ratio=0.03,
|
|
147
215
|
gradient_checkpointing=True,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "blitz-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Developer CLI for Blitz: local-first tracing/init, codebase instrumentation scan, and scaffolds for both a QLoRA workflow trainer and a coding-agent session-to-fine-tuning-corpus watcher."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -36,7 +36,7 @@ def test_build_config_carries_base_model_and_workflow():
|
|
|
36
36
|
|
|
37
37
|
def test_seq_len_is_clamped():
|
|
38
38
|
assert _cfg(seq_len=4096)["seq_len"] == 4096
|
|
39
|
-
assert _cfg(seq_len=999999)["seq_len"] ==
|
|
39
|
+
assert _cfg(seq_len=999999)["seq_len"] == 16384 # upper clamp
|
|
40
40
|
assert _cfg(seq_len=10)["seq_len"] == 1024 # lower clamp
|
|
41
41
|
assert _cfg(seq_len=0)["seq_len"] == 2048 # falsy -> default
|
|
42
42
|
|
|
@@ -45,6 +45,8 @@ def test_clamp_seq_len_helper():
|
|
|
45
45
|
assert _scaffold.clamp_seq_len(1200) == 1200
|
|
46
46
|
assert _scaffold.clamp_seq_len(None) == 2048
|
|
47
47
|
assert _scaffold.clamp_seq_len(50) == 1024
|
|
48
|
+
assert _scaffold.clamp_seq_len(16384) == 16384
|
|
49
|
+
assert _scaffold.clamp_seq_len(999999) == 16384
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
def test_gated_license_warning():
|
|
@@ -64,6 +66,31 @@ def test_build_config_eval_fraction_override():
|
|
|
64
66
|
assert cfg["eval_fraction"] == 0.05
|
|
65
67
|
|
|
66
68
|
|
|
69
|
+
def test_apply_batch_override_holds_effective_batch_size_steady():
|
|
70
|
+
# params_b=7 bucket: per_device_batch_size=2, grad_accum=8 -> effective 16
|
|
71
|
+
cfg = _cfg(params_b=7.0)
|
|
72
|
+
overridden = _scaffold.apply_batch_override(cfg, 1)
|
|
73
|
+
assert overridden["per_device_batch_size"] == 1
|
|
74
|
+
assert overridden["grad_accum"] == 16 # 2*8 / 1, effective batch unchanged
|
|
75
|
+
# only batch/accum change -- lora_r and everything else carry over as-is
|
|
76
|
+
assert overridden["lora_r"] == cfg["lora_r"]
|
|
77
|
+
assert overridden["base_model_hf"] == cfg["base_model_hf"]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_apply_batch_override_rounds_when_not_evenly_divisible():
|
|
81
|
+
cfg = _cfg(params_b=3.0) # per_device_batch_size=4, grad_accum=4 -> effective 16
|
|
82
|
+
overridden = _scaffold.apply_batch_override(cfg, 3)
|
|
83
|
+
assert overridden["per_device_batch_size"] == 3
|
|
84
|
+
assert overridden["grad_accum"] == 5 # round(16/3) = round(5.33) = 5
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_apply_batch_override_never_drops_grad_accum_below_one():
|
|
88
|
+
cfg = _cfg(params_b=3.0) # effective batch 16
|
|
89
|
+
overridden = _scaffold.apply_batch_override(cfg, 64) # way bigger than effective batch
|
|
90
|
+
assert overridden["grad_accum"] == 1
|
|
91
|
+
assert overridden["per_device_batch_size"] == 64
|
|
92
|
+
|
|
93
|
+
|
|
67
94
|
def test_render_writes_a_runnable_skeleton_without_config(tmp_path):
|
|
68
95
|
out = tmp_path / "train"
|
|
69
96
|
_scaffold.render(out, "wf")
|
|
@@ -185,6 +212,55 @@ def test_render_code_assist_train_py_reads_activity_db_and_defaults_to_qwen(tmp_
|
|
|
185
212
|
assert "apply_chat_template" in train_py
|
|
186
213
|
|
|
187
214
|
|
|
215
|
+
def test_render_code_assist_train_py_seq_len_defaults_to_16384(tmp_path):
|
|
216
|
+
out = tmp_path / "code-assist"
|
|
217
|
+
_scaffold.render_code_assist(out)
|
|
218
|
+
train_py = (out / "train.py").read_text()
|
|
219
|
+
assert 'os.environ.get("SEQ_LEN", "16384")' in train_py
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_render_code_assist_train_py_wires_tools_into_chat_template(tmp_path):
|
|
223
|
+
out = tmp_path / "code-assist"
|
|
224
|
+
_scaffold.render_code_assist(out)
|
|
225
|
+
train_py = (out / "train.py").read_text()
|
|
226
|
+
assert "_tool_schemas" in train_py
|
|
227
|
+
assert "tool_names <= _ALL_TOOL_NAMES" in train_py
|
|
228
|
+
assert "tools=tools" in train_py
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_render_code_assist_train_py_checkpoints_by_steps_not_epoch(tmp_path):
|
|
232
|
+
out = tmp_path / "code-assist"
|
|
233
|
+
_scaffold.render_code_assist(out)
|
|
234
|
+
train_py = (out / "train.py").read_text()
|
|
235
|
+
assert 'save_strategy="steps"' in train_py
|
|
236
|
+
assert "save_steps=_SAVE_STEPS" in train_py
|
|
237
|
+
assert 'save_strategy="epoch"' not in train_py
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_render_code_assist_train_py_parses_tool_call_arguments_before_templating(tmp_path):
|
|
241
|
+
# regression test: _codeassist.turn_to_messages stores tool_calls[i].function
|
|
242
|
+
# .arguments as a JSON *string* (correct -- matches the real OpenAI wire
|
|
243
|
+
# format), but Qwen's chat template does `arguments | tojson`, which
|
|
244
|
+
# assumes a raw object and double-encodes an already-stringified value.
|
|
245
|
+
# Confirmed on a real corpus example: training text ended up with escaped
|
|
246
|
+
# JSON-inside-a-string garbage (e.g. `"arguments": "{\"file_path\": ...}"`
|
|
247
|
+
# instead of a clean nested object), which correlated with abnormally high
|
|
248
|
+
# training loss. to_text must parse arguments back to a dict first.
|
|
249
|
+
out = tmp_path / "code-assist"
|
|
250
|
+
_scaffold.render_code_assist(out)
|
|
251
|
+
train_py = (out / "train.py").read_text()
|
|
252
|
+
assert 'isinstance(fn.get("arguments"), str)' in train_py
|
|
253
|
+
assert 'fn["arguments"] = json.loads(fn["arguments"])' in train_py
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_render_code_assist_train_py_wires_up_batch_override(tmp_path):
|
|
257
|
+
out = tmp_path / "code-assist"
|
|
258
|
+
_scaffold.render_code_assist(out)
|
|
259
|
+
train_py = (out / "train.py").read_text()
|
|
260
|
+
assert 'os.environ.get("PER_DEVICE_BATCH_SIZE")' in train_py
|
|
261
|
+
assert "_scaffold.apply_batch_override(cfg, int(_batch_override))" in train_py
|
|
262
|
+
|
|
263
|
+
|
|
188
264
|
def test_render_code_assist_makefile_substitutes_interval_base_model_and_bind_mounts(tmp_path):
|
|
189
265
|
out = tmp_path / "code-assist"
|
|
190
266
|
_scaffold.render_code_assist(out, interval=30, base_model="Qwen/Qwen2.5-Coder-7B-Instruct")
|
|
@@ -198,15 +274,22 @@ def test_render_code_assist_makefile_substitutes_interval_base_model_and_bind_mo
|
|
|
198
274
|
assert "--restart unless-stopped" in mk
|
|
199
275
|
assert "--gpus all" in mk
|
|
200
276
|
assert "Dockerfile.train" in mk
|
|
277
|
+
# free fragmentation-avoidance win for the trainer -- verified on real
|
|
278
|
+
# hardware to help (though not by itself sufficient for a 14B model on a
|
|
279
|
+
# 16GB card, see the 7B default above)
|
|
280
|
+
assert "PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True" in mk
|
|
201
281
|
for target in ("build:", "run:", "logs:", "stop:", "shell:", "build-train:", "train:"):
|
|
202
282
|
assert target in mk
|
|
203
283
|
|
|
204
284
|
|
|
205
|
-
def
|
|
285
|
+
def test_render_code_assist_defaults_to_qwen_7b_instruct(tmp_path):
|
|
286
|
+
# 7B, not 14B: the size class that comfortably fits a QLoRA run on a
|
|
287
|
+
# 16GB-class consumer card -- 14B was found (on real hardware) to OOM
|
|
288
|
+
# during LoRA adapter setup, before any training step even ran.
|
|
206
289
|
out = tmp_path / "code-assist"
|
|
207
290
|
_scaffold.render_code_assist(out)
|
|
208
291
|
mk = (out / "Makefile").read_text()
|
|
209
|
-
assert "BASE_MODEL_HF ?= Qwen/Qwen2.5-Coder-
|
|
292
|
+
assert "BASE_MODEL_HF ?= Qwen/Qwen2.5-Coder-7B-Instruct" in mk
|
|
210
293
|
|
|
211
294
|
|
|
212
295
|
def test_render_code_assist_readme_substitutes_interval_and_base_model(tmp_path):
|
|
@@ -229,7 +312,7 @@ def test_render_code_assist_appends_blitz_cli_pin_to_both_requirements_files(tmp
|
|
|
229
312
|
assert "pip install --no-cache-dir -r requirements.txt" in dockerfile
|
|
230
313
|
assert "_vendor" not in dockerfile
|
|
231
314
|
train_dockerfile = (out / "Dockerfile.train").read_text()
|
|
232
|
-
assert "pip install --no-cache-dir -r requirements-train.txt" in train_dockerfile
|
|
315
|
+
assert "pip install --no-cache-dir --break-system-packages -r requirements-train.txt" in train_dockerfile
|
|
233
316
|
assert "_vendor" not in train_dockerfile
|
|
234
317
|
|
|
235
318
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from blitz_cli import _tool_schemas
|
|
4
|
+
|
|
5
|
+
# The 24 tools this module is expected to cover: the 11 vanilla Claude Code
|
|
6
|
+
# tools plus the 13 harness-native tools sampled from the real corpus.
|
|
7
|
+
_EXPECTED_TOOLS = {
|
|
8
|
+
"Read", "Edit", "Write", "Bash", "Grep", "Glob", "Agent", "TodoWrite",
|
|
9
|
+
"WebSearch", "WebFetch", "AskUserQuestion",
|
|
10
|
+
"DesignSync", "EnterPlanMode", "ExitPlanMode", "Monitor", "ScheduleWakeup",
|
|
11
|
+
"SendMessage", "Skill", "TaskCreate", "TaskList", "TaskOutput", "TaskStop",
|
|
12
|
+
"TaskUpdate", "ToolSearch",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_tool_schemas_covers_all_24_expected_tools():
|
|
17
|
+
assert set(_tool_schemas.TOOL_SCHEMAS) == _EXPECTED_TOOLS
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_every_schema_has_the_expected_shape_and_round_trips_through_json():
|
|
21
|
+
for name, schema in _tool_schemas.TOOL_SCHEMAS.items():
|
|
22
|
+
assert schema["type"] == "function"
|
|
23
|
+
fn = schema["function"]
|
|
24
|
+
assert fn["name"] == name
|
|
25
|
+
assert isinstance(fn["description"], str) and fn["description"]
|
|
26
|
+
params = fn["parameters"]
|
|
27
|
+
assert params["type"] == "object"
|
|
28
|
+
assert isinstance(params["properties"], dict)
|
|
29
|
+
assert isinstance(params["required"], list)
|
|
30
|
+
# every required field must actually be a declared property
|
|
31
|
+
assert set(params["required"]) <= set(params["properties"])
|
|
32
|
+
# plain JSON-able data, no stray non-serializable values
|
|
33
|
+
assert json.loads(json.dumps(schema)) == schema
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_read_matches_activity_py_capture():
|
|
37
|
+
props = _tool_schemas.TOOL_SCHEMAS["Read"]["function"]["parameters"]
|
|
38
|
+
assert set(props["properties"]) == {"file_path", "offset", "limit"}
|
|
39
|
+
assert props["required"] == ["file_path"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_edit_matches_activity_py_capture():
|
|
43
|
+
props = _tool_schemas.TOOL_SCHEMAS["Edit"]["function"]["parameters"]
|
|
44
|
+
assert set(props["properties"]) == {"file_path", "old_string", "new_string", "replace_all"}
|
|
45
|
+
assert set(props["required"]) == {"file_path", "old_string", "new_string"}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_write_matches_activity_py_capture():
|
|
49
|
+
props = _tool_schemas.TOOL_SCHEMAS["Write"]["function"]["parameters"]
|
|
50
|
+
assert set(props["properties"]) == {"file_path", "content"}
|
|
51
|
+
assert set(props["required"]) == {"file_path", "content"}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_bash_matches_activity_py_capture():
|
|
55
|
+
props = _tool_schemas.TOOL_SCHEMAS["Bash"]["function"]["parameters"]
|
|
56
|
+
assert set(props["properties"]) == {"command", "timeout", "description", "run_in_background"}
|
|
57
|
+
assert props["required"] == ["command"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_grep_matches_activity_py_capture():
|
|
61
|
+
props = _tool_schemas.TOOL_SCHEMAS["Grep"]["function"]["parameters"]
|
|
62
|
+
assert set(props["properties"]) == {
|
|
63
|
+
"pattern", "path", "-i", "-n", "-A", "-B", "-C",
|
|
64
|
+
"output_mode", "type", "head_limit", "multiline",
|
|
65
|
+
}
|
|
66
|
+
assert props["required"] == ["pattern"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_agent_requires_all_three_fields():
|
|
70
|
+
props = _tool_schemas.TOOL_SCHEMAS["Agent"]["function"]["parameters"]
|
|
71
|
+
assert set(props["properties"]) == {"description", "prompt", "subagent_type"}
|
|
72
|
+
assert set(props["required"]) == {"description", "prompt", "subagent_type"}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_todowrite_is_a_full_list_replace_array():
|
|
76
|
+
props = _tool_schemas.TOOL_SCHEMAS["TodoWrite"]["function"]["parameters"]
|
|
77
|
+
assert set(props["properties"]) == {"todos"}
|
|
78
|
+
todos_schema = props["properties"]["todos"]
|
|
79
|
+
assert todos_schema["type"] == "array"
|
|
80
|
+
assert set(todos_schema["items"]["properties"]) == {"content", "status", "activeForm"}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_askuserquestion_nested_options_shape():
|
|
84
|
+
props = _tool_schemas.TOOL_SCHEMAS["AskUserQuestion"]["function"]["parameters"]
|
|
85
|
+
questions_schema = props["properties"]["questions"]
|
|
86
|
+
item_props = questions_schema["items"]["properties"]
|
|
87
|
+
assert set(item_props) == {"question", "header", "multiSelect", "options"}
|
|
88
|
+
option_props = item_props["options"]["items"]["properties"]
|
|
89
|
+
assert set(option_props) == {"label", "description"}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_taskupdate_uses_taskid_camelcase_not_task_id():
|
|
93
|
+
# a real inconsistency observed in the corpus vs TaskOutput/TaskStop's
|
|
94
|
+
# task_id -- kept faithfully rather than silently normalized.
|
|
95
|
+
props = _tool_schemas.TOOL_SCHEMAS["TaskUpdate"]["function"]["parameters"]
|
|
96
|
+
assert "taskId" in props["properties"]
|
|
97
|
+
assert "task_id" not in props["properties"]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_taskoutput_and_taskstop_use_task_id_snakecase():
|
|
101
|
+
for name in ("TaskOutput", "TaskStop"):
|
|
102
|
+
props = _tool_schemas.TOOL_SCHEMAS[name]["function"]["parameters"]
|
|
103
|
+
assert "task_id" in props["properties"]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_zero_arg_tools_have_empty_properties_and_required():
|
|
107
|
+
for name in ("EnterPlanMode", "TaskList"):
|
|
108
|
+
props = _tool_schemas.TOOL_SCHEMAS[name]["function"]["parameters"]
|
|
109
|
+
assert props["properties"] == {}
|
|
110
|
+
assert props["required"] == []
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_all_tools_is_sorted_by_name_with_one_entry_per_schema():
|
|
114
|
+
names = [t["function"]["name"] for t in _tool_schemas.ALL_TOOLS]
|
|
115
|
+
assert names == sorted(names)
|
|
116
|
+
assert len(_tool_schemas.ALL_TOOLS) == len(_tool_schemas.TOOL_SCHEMAS)
|
|
117
|
+
assert set(names) == _EXPECTED_TOOLS
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# Heavy QLoRA trainer for the code-assist corpus (data/activity.db). Kept as
|
|
2
|
-
# a SEPARATE image from the lightweight watcher (Dockerfile) -- the always-on
|
|
3
|
-
# watcher just polls the filesystem and doesn't need a GPU or the ML stack;
|
|
4
|
-
# baking that stack into the same image it runs continuously would bloat a
|
|
5
|
-
# daemon meant to idle for weeks. This image is only built/run on demand for
|
|
6
|
-
# `blitz code-assist train`.
|
|
7
|
-
FROM pytorch/pytorch:2.5.1-cuda12.1-cudnn9-runtime
|
|
8
|
-
|
|
9
|
-
WORKDIR /workspace
|
|
10
|
-
ENV PYTHONUNBUFFERED=1 HF_HOME=/workspace/.hf
|
|
11
|
-
|
|
12
|
-
COPY requirements-train.txt .
|
|
13
|
-
RUN pip install --no-cache-dir -r requirements-train.txt
|
|
14
|
-
|
|
15
|
-
COPY train.py .
|
|
16
|
-
|
|
17
|
-
CMD ["python", "train.py"]
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# QLoRA SFT stack for training on the code-assist corpus (data/activity.db),
|
|
2
|
-
# pinned to the same mutually-compatible set as the QLoRA workflow trainer's
|
|
3
|
-
# requirements.txt (CUDA 12.1, early 2026). torch is intentionally omitted --
|
|
4
|
-
# the Dockerfile.train's pytorch/cuda base image ships the matching CUDA
|
|
5
|
-
# build, and reinstalling risks pulling a CPU-only wheel.
|
|
6
|
-
transformers==4.48.0
|
|
7
|
-
trl==0.13.0
|
|
8
|
-
peft==0.14.0
|
|
9
|
-
accelerate==1.2.1
|
|
10
|
-
bitsandbytes==0.45.0
|
|
11
|
-
datasets==3.2.0
|
|
12
|
-
sentencepiece==0.2.0
|
|
13
|
-
# Local SQLite tracking/registry for this run (sqlite:///training.db) -- no
|
|
14
|
-
# `mlflow server` process, just the tracking client + SQLAlchemy backend.
|
|
15
|
-
mlflow==2.19.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|