Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions docker/OpenShell-RSI-Training.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
# Training-only extension. It is selected only by OpenShell training/model-evaluation jobs.
# It provides the pinned torch stack only; the trainer scripts ship inside each
# job's snapshot bundle from the frozen base commit (guest /opt is unreachable).
FROM headlesscode-openshell-rsi:local

USER root
# gcc + libc/Python headers: torch's triton JIT compiles a CUDA driver shim
# at forward time inside the guest.
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 python3-pip \
&& apt-get install -y --no-install-recommends python3 python3-pip python3-dev gcc libc6-dev \
&& rm -rf /var/lib/apt/lists/*
COPY scripts/rsi-training/requirements.lock /opt/headlesscode-rsi-training/requirements.lock
RUN python3 -m pip install --break-system-packages --no-cache-dir --require-hashes -r /opt/headlesscode-rsi-training/requirements.lock
RUN python3 -c "import torch; assert torch.version.cuda is not None, 'RSI training image requires a CUDA-enabled torch wheel'"
COPY scripts/rsi-training/train_qlora.py /opt/headlesscode-rsi-training/train_qlora.py
COPY scripts/rsi-training/evaluate_adapter.py /opt/headlesscode-rsi-training/evaluate_adapter.py
RUN chmod 0555 /opt/headlesscode-rsi-training/train_qlora.py /opt/headlesscode-rsi-training/evaluate_adapter.py \
&& chmod -R a-w /opt/headlesscode-rsi-training
RUN chmod -R a-w /opt/headlesscode-rsi-training

USER sandbox
8 changes: 6 additions & 2 deletions scripts/rsi-training/train_qlora.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,12 @@ def main() -> int:
raise RuntimeError("training loss became non-finite")
adapter_dir = output / "adapter"
model.save_pretrained(adapter_dir, safe_serialization=True)
if not (adapter_dir / "adapter_config.json").is_file() or not (adapter_dir / "adapter_model.safetensors").is_file():
raise RuntimeError("training ended without a complete PEFT adapter artifact")
readme = adapter_dir / "README.md"
if readme.is_file():
readme.unlink()
names = sorted(path.name for path in adapter_dir.iterdir() if path.is_file())
if names != ["adapter_config.json", "adapter_model.safetensors"]:
raise RuntimeError("training ended without exactly the fixed PEFT adapter file set")
result.update({
"status": "completed",
"stepsCompleted": steps,
Expand Down
34 changes: 31 additions & 3 deletions src/rsi/openshell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -697,9 +697,30 @@ export async function runOpenShellModelJob(candidate: CandidateRecord, config: R
if (training && (task.maxSteps !== 8 || task.adapterPath || task.modelKind)) throw new Error("training job differs from the bounded QLoRA policy")
if (!training && (!task.modelKind || !task.cellIds.length || (task.modelKind === "adapter" && !task.adapterPath) || (task.modelKind === "base" && task.adapterPath))) throw new Error("model evaluation job is missing or mismatches its fixed cell/model identity")
const output = "/workspace/.headlesscode-rsi-model-output"
const scripts = "/workspace/scripts/rsi-training"
const modelDir = "/workspace/.headlesscode-rsi-model"
const base = "/workspace/.rsi-base-model"
const shared = [
`--base-model ${base}`,
`--base-model-id '${task.baseModelId}'`,
]
const command = training
? `python3 /opt/headlesscode-rsi-training/train_qlora.py --base-model /workspace/.rsi-base-model --base-model-id '${task.baseModelId}' --dataset /workspace/.headlesscode-rsi-model/dataset.jsonl --manifest /workspace/.headlesscode-rsi-model/manifest.json --output /workspace/.headlesscode-rsi-model-output`
: `python3 /opt/headlesscode-rsi-training/evaluate_adapter.py --base-model /workspace/.rsi-base-model --base-model-id '${task.baseModelId}' --cells /workspace/.headlesscode-rsi-model/evaluation-cells.jsonl --output ${output}/evaluation-result.json${task.modelKind === "adapter" ? " --adapter-archive /workspace/.headlesscode-rsi-model/adapter.tar" : ""}`
? [
`python3 ${scripts}/train_qlora.py`,
...shared,
`--dataset ${modelDir}/dataset.jsonl`,
`--manifest ${modelDir}/manifest.json`,
`--output ${output}`,
].join(" ")
: [
`python3 ${scripts}/evaluate_adapter.py`,
...shared,
`--cells ${modelDir}/evaluation-cells.jsonl`,
`--output ${output}/evaluation-result.json`,
...(task.modelKind === "adapter"
? [`--adapter-archive ${modelDir}/adapter.tar`]
: []),
].join(" ")
const isolated = await runSnapshot({
config,
sourceRoot: candidate.worktree,
Expand All @@ -716,7 +737,14 @@ export async function runOpenShellModelJob(candidate: CandidateRecord, config: R
try {
const resultName = training ? "training-result.json" : "evaluation-result.json"
const resultPath = path.join(isolated.snapshot, ".headlesscode-rsi-model-output", resultName)
if (!fs.existsSync(resultPath)) throw new Error(`OpenShell model job omitted ${resultName}; exit=${isolated.result.exitCode}`)
if (!fs.existsSync(resultPath)) {
const guest = isolated.result.stdout.trim() || "(empty guest output)"
const detail = mutationFailureDiagnostic(guest)
const summary = detail.replaceAll("\n", " | ")
const omitted = `OpenShell model job omitted ${resultName}`
const code = `exit=${isolated.result.exitCode}`
throw new Error(`${omitted}; ${code}; guest output: ${summary}`)
}
const result = JSON.parse(fs.readFileSync(resultPath, "utf8")) as Record<string, unknown>
if (result.schemaVersion !== 1 || !["completed", "failed", "partial"].includes(String(result.status))) throw new Error("OpenShell model job returned an invalid result record")
if (training && result.status === "completed") {
Expand Down
Loading