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.
Files changed (68) hide show
  1. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/PKG-INFO +1 -1
  2. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/__init__.py +1 -1
  3. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_scaffold.py +33 -3
  4. blitz_cli-0.6.0/blitz_cli/_tool_schemas.py +189 -0
  5. blitz_cli-0.6.0/blitz_cli/templates/codeassist_Dockerfile.train.tmpl +26 -0
  6. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/codeassist_Makefile.tmpl +1 -0
  7. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/codeassist_README.md.tmpl +4 -1
  8. blitz_cli-0.6.0/blitz_cli/templates/codeassist_requirements_train.txt.tmpl +17 -0
  9. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/codeassist_train.py.tmpl +79 -11
  10. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/pyproject.toml +1 -1
  11. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_scaffold.py +87 -4
  12. blitz_cli-0.6.0/tests/test_tool_schemas.py +117 -0
  13. blitz_cli-0.5.0/blitz_cli/templates/codeassist_Dockerfile.train.tmpl +0 -17
  14. blitz_cli-0.5.0/blitz_cli/templates/codeassist_requirements_train.txt.tmpl +0 -15
  15. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/.github/workflows/homebrew.yml +0 -0
  16. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/.github/workflows/publish.yml +0 -0
  17. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/.gitignore +0 -0
  18. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/LICENSE +0 -0
  19. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/README.md +0 -0
  20. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_activity.py +0 -0
  21. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_client.py +0 -0
  22. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_codeassist.py +0 -0
  23. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_dataset.py +0 -0
  24. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_local.py +0 -0
  25. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_make.py +0 -0
  26. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_redact.py +0 -0
  27. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_release.py +0 -0
  28. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_scan/__init__.py +0 -0
  29. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_scan/_classify.py +0 -0
  30. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_scan/_js.py +0 -0
  31. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_scan/_models.py +0 -0
  32. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_scan/_python.py +0 -0
  33. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_scan/_report.py +0 -0
  34. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_scan/_walk.py +0 -0
  35. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_skill.py +0 -0
  36. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_sync.py +0 -0
  37. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_toml.py +0 -0
  38. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/_traces.py +0 -0
  39. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/cli.py +0 -0
  40. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/skills/__init__.py +0 -0
  41. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/skills/blitz-fix-teacher/SKILL.md +0 -0
  42. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/skills/blitz-instrument/SKILL.md +0 -0
  43. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/skills/blitz-setup-training/SKILL.md +0 -0
  44. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/skills/blitz-simulate-traces/SKILL.md +0 -0
  45. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/Dockerfile.tmpl +0 -0
  46. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/Makefile.tmpl +0 -0
  47. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/README.md.tmpl +0 -0
  48. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/__init__.py +0 -0
  49. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/codeassist_Dockerfile.tmpl +0 -0
  50. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/codeassist_dockerignore.tmpl +0 -0
  51. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/codeassist_requirements.txt.tmpl +0 -0
  52. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/dockerignore.tmpl +0 -0
  53. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/requirements.txt.tmpl +0 -0
  54. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/serve.py.tmpl +0 -0
  55. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/train.py.tmpl +0 -0
  56. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/blitz_cli/templates/watch.py.tmpl +0 -0
  57. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_activity.py +0 -0
  58. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_cli.py +0 -0
  59. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_codeassist.py +0 -0
  60. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_local.py +0 -0
  61. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_make.py +0 -0
  62. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_redact.py +0 -0
  63. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_release.py +0 -0
  64. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_scan.py +0 -0
  65. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_serve.py +0 -0
  66. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_sync.py +0 -0
  67. {blitz_cli-0.5.0 → blitz_cli-0.6.0}/tests/test_toml.py +0 -0
  68. {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.5.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
@@ -30,4 +30,4 @@ Commands:
30
30
 
31
31
  __all__ = ["__version__"]
32
32
 
33
- __version__ = "0.5.0"
33
+ __version__ = "0.6.0"
@@ -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
- return max(1024, min(8192, int(raw or 0) or 2048))
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-14B-Instruct"
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. Override
10
- via the BASE_MODEL_HF env var (also settable at scaffold time with
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", "4096")))
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
- print(f"Base model: {cfg['base_model_hf']} seq_len={cfg['seq_len']} params_b={cfg['params_b']}")
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 -- we don't pass a `tools=` schema list here
127
- # since Claude Code's own transcripts don't record each tool's JSON
128
- # input_schema, only the calls actually made.
129
- return {"text": tokenizer.apply_chat_template(example["messages"], tokenize=False)}
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
- save_strategy="epoch",
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.5.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"] == 8192 # upper clamp
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 test_render_code_assist_defaults_to_qwen_14b_instruct(tmp_path):
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-14B-Instruct" in mk
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