@ruvector/ruvllm 3.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GPU-LOOP.md +229 -0
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +159 -360
- package/examples/gpu-loop.example.json +26 -0
- package/package.json +50 -121
- package/src/adapters-cli.mjs +23 -0
- package/src/adapters.mjs +91 -0
- package/src/cli.mjs +30 -0
- package/src/engine.mjs +238 -0
- package/src/fixture.mjs +38 -0
- package/src/gpu-host.mjs +47 -0
- package/src/gpu-loop-cli.mjs +67 -0
- package/src/gpu-loop.mjs +184 -0
- package/src/gpu-spool-adapter.mjs +80 -0
- package/src/vast-host-adapter.mjs +274 -0
- package/bin/cli.js +0 -1040
- package/dist/cjs/benchmarks/embedding-benchmark.d.ts +0 -102
- package/dist/cjs/benchmarks/embedding-benchmark.d.ts.map +0 -1
- package/dist/cjs/benchmarks/embedding-benchmark.js +0 -436
- package/dist/cjs/benchmarks/index.d.ts +0 -34
- package/dist/cjs/benchmarks/index.d.ts.map +0 -1
- package/dist/cjs/benchmarks/index.js +0 -132
- package/dist/cjs/benchmarks/model-comparison.d.ts +0 -71
- package/dist/cjs/benchmarks/model-comparison.d.ts.map +0 -1
- package/dist/cjs/benchmarks/model-comparison.js +0 -476
- package/dist/cjs/benchmarks/routing-benchmark.d.ts +0 -70
- package/dist/cjs/benchmarks/routing-benchmark.d.ts.map +0 -1
- package/dist/cjs/benchmarks/routing-benchmark.js +0 -289
- package/dist/cjs/contrastive.d.ts +0 -229
- package/dist/cjs/contrastive.d.ts.map +0 -1
- package/dist/cjs/contrastive.js +0 -589
- package/dist/cjs/engine.d.ts +0 -160
- package/dist/cjs/engine.d.ts.map +0 -1
- package/dist/cjs/engine.js +0 -552
- package/dist/cjs/export.d.ts +0 -182
- package/dist/cjs/export.d.ts.map +0 -1
- package/dist/cjs/export.js +0 -422
- package/dist/cjs/federated.d.ts +0 -233
- package/dist/cjs/federated.d.ts.map +0 -1
- package/dist/cjs/federated.js +0 -525
- package/dist/cjs/index.d.ts +0 -68
- package/dist/cjs/index.d.ts.map +0 -1
- package/dist/cjs/index.js +0 -104
- package/dist/cjs/intelligence.d.ts +0 -136
- package/dist/cjs/intelligence.d.ts.map +0 -1
- package/dist/cjs/intelligence.js +0 -226
- package/dist/cjs/lora.d.ts +0 -251
- package/dist/cjs/lora.d.ts.map +0 -1
- package/dist/cjs/lora.js +0 -509
- package/dist/cjs/models.d.ts +0 -129
- package/dist/cjs/models.d.ts.map +0 -1
- package/dist/cjs/models.js +0 -323
- package/dist/cjs/native.d.ts +0 -140
- package/dist/cjs/native.d.ts.map +0 -1
- package/dist/cjs/native.js +0 -98
- package/dist/cjs/session.d.ts +0 -80
- package/dist/cjs/session.d.ts.map +0 -1
- package/dist/cjs/session.js +0 -203
- package/dist/cjs/simd.d.ts +0 -90
- package/dist/cjs/simd.d.ts.map +0 -1
- package/dist/cjs/simd.js +0 -209
- package/dist/cjs/sona.d.ts +0 -223
- package/dist/cjs/sona.d.ts.map +0 -1
- package/dist/cjs/sona.js +0 -562
- package/dist/cjs/streaming.d.ts +0 -55
- package/dist/cjs/streaming.d.ts.map +0 -1
- package/dist/cjs/streaming.js +0 -131
- package/dist/cjs/training.d.ts +0 -312
- package/dist/cjs/training.d.ts.map +0 -1
- package/dist/cjs/training.js +0 -610
- package/dist/cjs/types.d.ts +0 -696
- package/dist/cjs/types.d.ts.map +0 -1
- package/dist/cjs/types.js +0 -6
- package/dist/esm/benchmarks/embedding-benchmark.d.ts +0 -102
- package/dist/esm/benchmarks/embedding-benchmark.d.ts.map +0 -1
- package/dist/esm/benchmarks/embedding-benchmark.js +0 -427
- package/dist/esm/benchmarks/index.d.ts +0 -34
- package/dist/esm/benchmarks/index.d.ts.map +0 -1
- package/dist/esm/benchmarks/index.js +0 -110
- package/dist/esm/benchmarks/model-comparison.d.ts +0 -71
- package/dist/esm/benchmarks/model-comparison.d.ts.map +0 -1
- package/dist/esm/benchmarks/model-comparison.js +0 -434
- package/dist/esm/benchmarks/routing-benchmark.d.ts +0 -70
- package/dist/esm/benchmarks/routing-benchmark.d.ts.map +0 -1
- package/dist/esm/benchmarks/routing-benchmark.js +0 -283
- package/dist/esm/contrastive.d.ts +0 -229
- package/dist/esm/contrastive.d.ts.map +0 -1
- package/dist/esm/contrastive.js +0 -578
- package/dist/esm/engine.d.ts +0 -160
- package/dist/esm/engine.d.ts.map +0 -1
- package/dist/esm/engine.js +0 -548
- package/dist/esm/export.d.ts +0 -182
- package/dist/esm/export.d.ts.map +0 -1
- package/dist/esm/export.js +0 -414
- package/dist/esm/federated.d.ts +0 -233
- package/dist/esm/federated.d.ts.map +0 -1
- package/dist/esm/federated.js +0 -520
- package/dist/esm/index.d.ts +0 -68
- package/dist/esm/index.d.ts.map +0 -1
- package/dist/esm/index.js +0 -84
- package/dist/esm/intelligence.d.ts +0 -136
- package/dist/esm/intelligence.d.ts.map +0 -1
- package/dist/esm/intelligence.js +0 -188
- package/dist/esm/lora.d.ts +0 -251
- package/dist/esm/lora.d.ts.map +0 -1
- package/dist/esm/lora.js +0 -504
- package/dist/esm/models.d.ts +0 -129
- package/dist/esm/models.d.ts.map +0 -1
- package/dist/esm/models.js +0 -282
- package/dist/esm/native.d.ts +0 -140
- package/dist/esm/native.d.ts.map +0 -1
- package/dist/esm/native.js +0 -93
- package/dist/esm/session.d.ts +0 -80
- package/dist/esm/session.d.ts.map +0 -1
- package/dist/esm/session.js +0 -199
- package/dist/esm/simd.d.ts +0 -90
- package/dist/esm/simd.d.ts.map +0 -1
- package/dist/esm/simd.js +0 -205
- package/dist/esm/sona.d.ts +0 -223
- package/dist/esm/sona.d.ts.map +0 -1
- package/dist/esm/sona.js +0 -556
- package/dist/esm/streaming.d.ts +0 -55
- package/dist/esm/streaming.d.ts.map +0 -1
- package/dist/esm/streaming.js +0 -126
- package/dist/esm/training.d.ts +0 -312
- package/dist/esm/training.d.ts.map +0 -1
- package/dist/esm/training.js +0 -603
- package/dist/esm/types.d.ts +0 -696
- package/dist/esm/types.d.ts.map +0 -1
- package/dist/esm/types.js +0 -5
- package/scripts/ensemble-model-compare.js +0 -287
- package/scripts/fix-esm-extensions.mjs +0 -181
- package/scripts/huggingface/README.md +0 -537
- package/scripts/huggingface/publish.sh +0 -112
- package/scripts/hybrid-model-compare.js +0 -373
- package/scripts/improved-model-compare.js +0 -288
- package/scripts/optimized-model-compare.js +0 -364
- package/scripts/real-model-compare.js +0 -280
- package/scripts/smoke-esm.mjs +0 -82
- package/scripts/training/agentic-flow-capabilities.json +0 -329
- package/scripts/training/claude-code-synth.js +0 -505
- package/scripts/training/claude-flow-capabilities.json +0 -1322
- package/scripts/training/claude-hard-negatives.js +0 -222
- package/scripts/training/contrastive-finetune.js +0 -517
- package/scripts/training/ecosystem-triplets.jsonl +0 -1467
- package/scripts/training/generate-ecosystem-triplets.js +0 -1324
- package/scripts/training/routing-dataset.js +0 -634
- package/scripts/training/ruvector-capabilities.json +0 -798
- package/scripts/training/validate-ecosystem.js +0 -381
- package/scripts/training/validation-results.json +0 -23
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// Vast.ai host adapter for the scheduled GPU loop (ADR-015, ADR-017). Trusted
|
|
2
|
+
// operator code: it resolves the job's secretRef from an owner-only host config,
|
|
3
|
+
// rents exactly the configured offer, never reuses a shared instance, and only
|
|
4
|
+
// reports a verified completion after re-scoring the retrieved raw outputs here.
|
|
5
|
+
// No effect happens at import; `node vast-host-adapter.mjs guard ID REF` is the
|
|
6
|
+
// independent destroy timer armed for every rental.
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { constants, createWriteStream } from 'node:fs';
|
|
10
|
+
import { mkdir, open, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
11
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
12
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
export const HOST_CONFIG_ENV = 'RUVLLM_VAST_HOST_CONFIG';
|
|
16
|
+
const SELF = fileURLToPath(import.meta.url);
|
|
17
|
+
const INSPECT_BUDGET_MS = 24000;
|
|
18
|
+
const fail = code => { const error = new Error(code); error.code = code; throw error; };
|
|
19
|
+
const need = (ok, code) => { if (!ok) fail(code); };
|
|
20
|
+
const abs = value => typeof value === 'string' && isAbsolute(value) && resolve(value) === value;
|
|
21
|
+
const ceilMicro = usd => Math.ceil(usd * 1e6) / 1e6;
|
|
22
|
+
|
|
23
|
+
async function privateJson(path) {
|
|
24
|
+
need(abs(path), 'VAST_HOST_CONFIG_PATH');
|
|
25
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
26
|
+
try {
|
|
27
|
+
const info = await handle.stat();
|
|
28
|
+
need(info.isFile() && (info.mode & 0o077) === 0 && info.uid === process.getuid() && info.size <= 65536, 'VAST_HOST_CONFIG_UNSAFE');
|
|
29
|
+
const bytes = await handle.readFile();
|
|
30
|
+
return { value: JSON.parse(bytes), sha256: createHash('sha256').update(bytes).digest('hex') };
|
|
31
|
+
} finally { await handle.close(); }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function readHostConfig(path = process.env[HOST_CONFIG_ENV]) {
|
|
35
|
+
const { value: config, sha256 } = await privateJson(path);
|
|
36
|
+
need(config?.schema === 'ruvllm-vast-host/v1', 'VAST_HOST_CONFIG_SCHEMA');
|
|
37
|
+
need(/^https:\/\/[a-z0-9.-]+$/.test(config.apiBase), 'VAST_HOST_CONFIG_API');
|
|
38
|
+
need(/^[a-z0-9./_-]+@sha256:[a-f0-9]{64}$/.test(config.image), 'VAST_HOST_CONFIG_IMAGE');
|
|
39
|
+
need(/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\.git$/.test(config.repoUrl) && /^[a-f0-9]{40}$/.test(config.commit), 'VAST_HOST_CONFIG_SOURCE');
|
|
40
|
+
need(/^v[0-9]+$/.test(config.bundle), 'VAST_HOST_CONFIG_BUNDLE');
|
|
41
|
+
need(Number.isInteger(config.diskGb) && config.diskGb >= 20 && config.diskGb <= 200, 'VAST_HOST_CONFIG_DISK');
|
|
42
|
+
need(Number.isInteger(config.armDeadlineSeconds) && config.armDeadlineSeconds >= 60 && config.armDeadlineSeconds <= 43200, 'VAST_HOST_CONFIG_DEADLINE');
|
|
43
|
+
need(Number.isInteger(config.guardGraceSeconds) && config.guardGraceSeconds >= 60 && config.guardGraceSeconds <= 3600, 'VAST_HOST_CONFIG_GUARD');
|
|
44
|
+
need(['resultsRoot', 'sshKey', 'repoRoot', 'python', 'node'].every(key => abs(config[key])), 'VAST_HOST_CONFIG_PATHS');
|
|
45
|
+
need(config.adapterDir === undefined || (/^[a-z0-9][a-z0-9._-]{0,63}$/.test(config.adapterDir) && !config.adapterDir.includes('..')), 'VAST_HOST_CONFIG_ADAPTER');
|
|
46
|
+
need(config.trainPayload === undefined || /^experiments\/model-improvement\/training\/[a-z0-9][a-z0-9._-]{0,63}$/.test(config.trainPayload), 'VAST_HOST_CONFIG_PAYLOAD');
|
|
47
|
+
need(config.secrets && typeof config.secrets === 'object' && Object.values(config.secrets).every(entry => abs(entry?.file)), 'VAST_HOST_CONFIG_SECRETS');
|
|
48
|
+
return Object.freeze({ ...config, sha256 });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function apiKey(config, ref) {
|
|
52
|
+
const entry = Object.hasOwn(config.secrets, ref) ? config.secrets[ref] : null;
|
|
53
|
+
need(entry, 'VAST_SECRET_REF_UNKNOWN');
|
|
54
|
+
const { mode, uid } = await stat(entry.file);
|
|
55
|
+
need((mode & 0o077) === 0 && uid === process.getuid(), 'VAST_SECRET_FILE_UNSAFE');
|
|
56
|
+
const key = (await readFile(entry.file, 'utf8')).trim();
|
|
57
|
+
need(/^[A-Za-z0-9_-]{16,256}$/.test(key), 'VAST_SECRET_INVALID');
|
|
58
|
+
return key;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Bounded child process without a shell. Output is collected, never echoed.
|
|
62
|
+
function defaultRun(command, args, { timeoutMs, cwd, stdoutFile, input } = {}) {
|
|
63
|
+
return new Promise(resolvePromise => {
|
|
64
|
+
const child = spawn(command, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
65
|
+
const chunks = [], errors = [];
|
|
66
|
+
let size = 0;
|
|
67
|
+
const sink = stdoutFile ? createWriteStream(stdoutFile, { flags: 'wx', mode: 0o600 }) : null;
|
|
68
|
+
const flushed = sink ? new Promise(done => { sink.on('close', done); sink.on('error', done); }) : Promise.resolve();
|
|
69
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), Math.max(1, timeoutMs ?? 10000));
|
|
70
|
+
if (sink) child.stdout.pipe(sink);
|
|
71
|
+
else child.stdout.on('data', chunk => { size += chunk.length; if (size <= 8 << 20) chunks.push(chunk); });
|
|
72
|
+
child.stderr.on('data', chunk => { if (errors.length < 64) errors.push(chunk); });
|
|
73
|
+
child.on('error', () => {});
|
|
74
|
+
child.on('close', async code => {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
await flushed;
|
|
77
|
+
resolvePromise({ code, stdout: Buffer.concat(chunks).toString('utf8'), stderr: Buffer.concat(errors).toString('utf8') });
|
|
78
|
+
});
|
|
79
|
+
child.stdin.end(input ?? '');
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function writePrivate(path, value) {
|
|
84
|
+
await writeFile(path, typeof value === 'string' ? value : JSON.stringify(value, null, 2) + '\n', { mode: 0o600 });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function onstartScript(config, task = 'evaluate') {
|
|
88
|
+
const scripts = '/workspace/ruvllm/experiments/model-improvement/vast';
|
|
89
|
+
if (task === 'train') need(config.trainPayload, 'VAST_HOST_CONFIG_PAYLOAD');
|
|
90
|
+
const run = task === 'train'
|
|
91
|
+
? `${scripts}/remote-train.sh ${config.commit} ${config.armDeadlineSeconds} ${config.trainPayload}`
|
|
92
|
+
: `${scripts}/remote-run.sh ${config.commit} ${config.armDeadlineSeconds} ${config.bundle} ${config.adapterDir ?? 'coding-4b'}`;
|
|
93
|
+
const failed = `grep -q '"failed"' /workspace/run/status.json 2>/dev/null || printf '{"schema":"ruvllm-vast-remote-status/v1","state":"failed","step":"bootstrap"}\\n' > /workspace/run/status.json`;
|
|
94
|
+
return ['#!/bin/bash', 'mkdir -p /workspace/run',
|
|
95
|
+
`nohup bash -c '(export DEBIAN_FRONTEND=noninteractive; (command -v git >/dev/null || (apt-get update -qq && apt-get install -y -qq git >/dev/null)) && git clone --quiet ${config.repoUrl} /workspace/ruvllm && git -C /workspace/ruvllm checkout --quiet ${config.commit} && bash ${run}) >/workspace/run/remote.log 2>&1 || { ${failed.replaceAll("'", "'\\''")}; }' >/dev/null 2>&1 &`, ''].join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function createVastAdapter({ fetchImpl = globalThis.fetch, run = defaultRun, now = () => Date.now(), readConfig = readHostConfig } = {}) {
|
|
99
|
+
async function api(config, key, method, path, { body, query, signal } = {}) {
|
|
100
|
+
const url = new URL(config.apiBase + path);
|
|
101
|
+
for (const [name, value] of Object.entries(query ?? {})) url.searchParams.set(name, JSON.stringify(value));
|
|
102
|
+
const response = await fetchImpl(url, { method, signal, headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
|
103
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }) });
|
|
104
|
+
if (response.status === 404) return null;
|
|
105
|
+
need(response.ok, `VAST_HTTP_${response.status}`);
|
|
106
|
+
return response.json();
|
|
107
|
+
}
|
|
108
|
+
async function instances(config, key, signal) {
|
|
109
|
+
const rows = [];
|
|
110
|
+
let after;
|
|
111
|
+
for (let page = 0; page < 40; page++) {
|
|
112
|
+
const data = await api(config, key, 'GET', '/api/v1/instances/', { signal,
|
|
113
|
+
query: { select_filters: {}, order_by: [{ col: 'id', dir: 'asc' }], limit: 25, ...(after ? { after_token: after } : {}) } });
|
|
114
|
+
rows.push(...(data?.instances ?? []));
|
|
115
|
+
if (!data?.next_token) return rows;
|
|
116
|
+
after = data.next_token;
|
|
117
|
+
}
|
|
118
|
+
fail('VAST_LIST_UNBOUNDED');
|
|
119
|
+
}
|
|
120
|
+
const attemptDir = (config, key) => join(config.resultsRoot, key);
|
|
121
|
+
const ssh = (config, instance, remote) => ['-i', config.sshKey, '-p', String(instance.ssh_port), '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=8',
|
|
122
|
+
'-o', 'StrictHostKeyChecking=accept-new', '-o', `UserKnownHostsFile=${join(config.resultsRoot, `known_hosts-${instance.id}`)}`, '-o', 'IdentitiesOnly=yes',
|
|
123
|
+
`root@${instance.ssh_host}`, remote];
|
|
124
|
+
|
|
125
|
+
async function submit({ job, idempotencyKey, signal }) {
|
|
126
|
+
const config = await readConfig(), key = await apiKey(config, job.secretRef);
|
|
127
|
+
need(typeof job.resource.offerId === 'string' && /^[0-9]{1,12}$/.test(job.resource.offerId), 'VAST_OFFER_REQUIRED');
|
|
128
|
+
const label = `ruvllm-${idempotencyKey}`;
|
|
129
|
+
const existing = (await instances(config, key, signal)).find(row => row.label === label);
|
|
130
|
+
if (existing) return { runId: String(existing.id) };
|
|
131
|
+
// The bundles search ignores `id`; `ask_contract_id` is the offer identity filter.
|
|
132
|
+
const offers = (await api(config, key, 'POST', '/api/v0/bundles/', { signal, body: { ask_contract_id: { eq: Number(job.resource.offerId) },
|
|
133
|
+
rentable: { eq: true }, type: 'on-demand', allocated_storage: config.diskGb, limit: 2 } }))?.offers ?? [];
|
|
134
|
+
need(offers.length === 1 && String(offers[0].id) === job.resource.offerId
|
|
135
|
+
&& Number.isFinite(offers[0].dph_total) && offers[0].dph_total > 0, 'VAST_OFFER_UNAVAILABLE');
|
|
136
|
+
const offer = offers[0];
|
|
137
|
+
// Spending ceiling: the worst case (full timeout at the offer's total hourly
|
|
138
|
+
// price, storage included) must fit the job's reserved ceiling before renting.
|
|
139
|
+
const worstCaseUsd = ceilMicro(offer.dph_total * job.timeoutSeconds / 3600);
|
|
140
|
+
need(worstCaseUsd <= job.maxCostUsd, 'VAST_COST_CEILING');
|
|
141
|
+
await mkdir(attemptDir(config, idempotencyKey), { mode: 0o700 });
|
|
142
|
+
const created = await api(config, key, 'PUT', `/api/v0/asks/${offer.id}/`, { signal, body: { client_id: 'me', image: config.image,
|
|
143
|
+
env: {}, price: null, disk: config.diskGb, label, onstart: onstartScript(config, job.task), runtype: 'ssh', force: false, cancel_unavail: true } });
|
|
144
|
+
need(created?.success === true && Number.isSafeInteger(created.new_contract), 'VAST_CREATE_FAILED');
|
|
145
|
+
const instanceId = created.new_contract;
|
|
146
|
+
// Independent of this scheduler: a user timer destroys the rental after
|
|
147
|
+
// timeout + grace even if no further tick ever runs.
|
|
148
|
+
const guard = await run('systemd-run', ['--user', '--quiet', `--unit=ruvllm-vast-guard-${instanceId}`,
|
|
149
|
+
`--on-active=${job.timeoutSeconds + config.guardGraceSeconds}s`, `--setenv=${HOST_CONFIG_ENV}=${process.env[HOST_CONFIG_ENV] ?? ''}`,
|
|
150
|
+
config.node, SELF, 'guard', String(instanceId), job.secretRef], { timeoutMs: 8000 });
|
|
151
|
+
await writePrivate(join(attemptDir(config, idempotencyKey), 'submission.json'), { schema: 'ruvllm-vast-submission/v1',
|
|
152
|
+
jobId: job.id, idempotencyKey, label, instanceId, offerId: offer.id, gpuName: offer.gpu_name, dphTotal: offer.dph_total,
|
|
153
|
+
worstCaseUsd, maxCostUsd: job.maxCostUsd, timeoutSeconds: job.timeoutSeconds, image: config.image, commit: config.commit,
|
|
154
|
+
task: job.task, bundle: config.bundle, adapterDir: config.adapterDir ?? 'coding-4b', trainPayload: config.trainPayload ?? null, hostConfigSha256: config.sha256, guardArmed: guard.code === 0, submittedAt: new Date(now()).toISOString() });
|
|
155
|
+
if (guard.code !== 0) {
|
|
156
|
+
await api(config, key, 'DELETE', `/api/v0/instances/${instanceId}/`, { body: {} });
|
|
157
|
+
fail('VAST_GUARD_UNAVAILABLE');
|
|
158
|
+
}
|
|
159
|
+
return { runId: String(instanceId) };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function retrieve(config, instance, dir, started) {
|
|
163
|
+
const left = () => INSPECT_BUDGET_MS - (now() - started);
|
|
164
|
+
const archive = join(dir, 'evidence.tar');
|
|
165
|
+
await rm(archive, { force: true }); await rm(join(dir, 'evidence'), { recursive: true, force: true });
|
|
166
|
+
if (left() < 4000) return false;
|
|
167
|
+
const pulled = await run('ssh', ssh(config, instance, 'tar -C /workspace -cf - out run'), { timeoutMs: left() - 3000, stdoutFile: archive });
|
|
168
|
+
if (pulled.code !== 0) return false;
|
|
169
|
+
await mkdir(join(dir, 'evidence'), { mode: 0o700 });
|
|
170
|
+
return (await run('tar', ['-xf', archive, '-C', join(dir, 'evidence'), '--no-same-owner', '--no-same-permissions'], { timeoutMs: 5000 })).code === 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function verify(config, job, dir) {
|
|
174
|
+
const evidence = join(dir, 'evidence', 'out'), scored = join(dir, 'local-scored');
|
|
175
|
+
await rm(scored, { recursive: true, force: true });
|
|
176
|
+
const bundle = join(config.repoRoot, 'experiments/model-improvement/frozen', config.bundle);
|
|
177
|
+
const result = await run(config.python, [join(config.repoRoot, 'experiments/model-improvement/measurement.py'), '--bundle', bundle,
|
|
178
|
+
'--base-run', join(evidence, 'base'), '--adapter-run', join(evidence, 'adapter'), '--out', scored], { timeoutMs: 15000, cwd: config.repoRoot });
|
|
179
|
+
if (result.code !== 0) return { ok: false, reason: 'local_rescore_failed' };
|
|
180
|
+
const local = JSON.parse(await readFile(join(scored, 'report.json'), 'utf8'));
|
|
181
|
+
const remote = JSON.parse(await readFile(join(evidence, 'scored', 'report.json'), 'utf8'));
|
|
182
|
+
const ok = isDeepStrictEqual(local, remote) && local.evidence_mode === 'gpu-inference';
|
|
183
|
+
await writePrivate(join(dir, 'verification.json'), { schema: 'ruvllm-vast-verification/v1', jobId: job.id,
|
|
184
|
+
localRescoreMatchesRemote: isDeepStrictEqual(local, remote), evidenceMode: local.evidence_mode,
|
|
185
|
+
researchImprovementConfirmed: local.research_improvement_confirmed, reasons: local.reasons });
|
|
186
|
+
return { ok, reason: ok ? 'verified' : 'report_mismatch' };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Training evidence: the trainer's own manifest must hash every retrieved file,
|
|
190
|
+
// bind the committed payload, and match the completed event in the event log.
|
|
191
|
+
async function verifyTraining(config, job, dir) {
|
|
192
|
+
const out = join(dir, 'evidence', 'out', 'training-v1');
|
|
193
|
+
const sha = async path => createHash('sha256').update(await readFile(path)).digest('hex');
|
|
194
|
+
const checks = {};
|
|
195
|
+
try {
|
|
196
|
+
const bridge = JSON.parse(await readFile(join(config.repoRoot, config.trainPayload, 'bridge.json'), 'utf8'));
|
|
197
|
+
const manifestBytes = await readFile(join(out, 'manifest.json'));
|
|
198
|
+
const manifest = JSON.parse(manifestBytes);
|
|
199
|
+
const events = (await readFile(join(dir, 'evidence', 'run', 'train.log'), 'utf8')).split('\n')
|
|
200
|
+
.filter(line => line.startsWith('RUFLO_TRAIN ')).map(line => JSON.parse(line.slice(12)));
|
|
201
|
+
const completed = events.find(event => event.kind === 'completed');
|
|
202
|
+
checks.payloadBound = manifest.training_sha256 === bridge.training_sha256;
|
|
203
|
+
checks.manifestEvent = completed?.data?.manifest_sha256 === createHash('sha256').update(manifestBytes).digest('hex');
|
|
204
|
+
checks.steps = manifest.steps === 96 && manifest.revision === job.model.revision && manifest.model === job.model.repo;
|
|
205
|
+
const files = Object.entries(manifest.files ?? {});
|
|
206
|
+
checks.files = files.length >= 6 && (await Promise.all(files.map(async ([name, digest]) =>
|
|
207
|
+
!name.split('/').includes('..') && await sha(join(out, name)) === digest))).every(Boolean);
|
|
208
|
+
checks.validation = events.filter(event => event.kind === 'validation').map(event => event.data);
|
|
209
|
+
} catch (error) { checks.error = error.code ?? error.name; }
|
|
210
|
+
const ok = Boolean(checks.payloadBound && checks.manifestEvent && checks.steps && checks.files && !checks.error);
|
|
211
|
+
await writePrivate(join(dir, 'verification.json'), { schema: 'ruvllm-vast-training-verification/v1', jobId: job.id, ...checks, ok });
|
|
212
|
+
return { ok };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function inspect({ job, runId, idempotencyKey, signal }) {
|
|
216
|
+
const started = now(), config = await readConfig(), key = await apiKey(config, job.secretRef);
|
|
217
|
+
const instance = (await instances(config, key, signal)).find(row => String(row.id) === runId);
|
|
218
|
+
if (!instance) return { status: 'failed' };
|
|
219
|
+
const costUsd = ceilMicro(instance.dph_total * Math.max(0, now() / 1000 - (instance.start_date ?? now() / 1000)) / 3600);
|
|
220
|
+
if (costUsd > job.maxCostUsd) return { status: 'failed', costUsd };
|
|
221
|
+
// A host whose container runtime cannot start the image never recovers; fail now instead of at the deadline.
|
|
222
|
+
if (instance.actual_status !== 'running' && /error response from daemon|oci runtime|invalid runtime/i.test(instance.status_msg ?? '')) return { status: 'failed', costUsd };
|
|
223
|
+
if (instance.actual_status !== 'running' || !instance.ssh_host || !instance.ssh_port) return { status: 'running', costUsd };
|
|
224
|
+
const dir = attemptDir(config, idempotencyKey);
|
|
225
|
+
const probe = await run('ssh', ssh(config, instance, `cat /workspace/run/status.json && echo && { tail -c 400 /workspace/run/${job.task === 'train' ? 'train' : 'run-pair'}.log 2>/dev/null || true; }`), { timeoutMs: 10000 });
|
|
226
|
+
if (probe.code !== 0) return { status: 'running', costUsd };
|
|
227
|
+
let remote;
|
|
228
|
+
try { remote = JSON.parse(probe.stdout.split('\n')[0]); } catch { return { status: 'running', costUsd }; }
|
|
229
|
+
await writePrivate(join(dir, 'progress.json'), { at: new Date(now()).toISOString(), costUsd, remote, tail: probe.stdout.split('\n').slice(1).join('\n').slice(-400) });
|
|
230
|
+
if (remote.state === 'running') return { status: 'running', costUsd };
|
|
231
|
+
if (remote.state === 'failed') { await retrieve(config, instance, dir, started); return { status: 'failed', costUsd }; }
|
|
232
|
+
if (remote.state !== 'done') return { status: 'running', costUsd };
|
|
233
|
+
// Retain raw outputs locally before any terminal report; retry next tick on a slow pull.
|
|
234
|
+
if (!await retrieve(config, instance, dir, started)) return { status: 'running', costUsd };
|
|
235
|
+
const checked = job.task === 'train' ? await verifyTraining(config, job, dir) : await verify(config, job, dir);
|
|
236
|
+
return { status: 'completed', costUsd, completion: { completionVersion: 1, completionVerified: checked.ok, status: checked.ok ? 'ok' : 'error',
|
|
237
|
+
exitCode: checked.ok ? 0 : 1, assertionsVerified: checked.ok, evidenceComplete: checked.ok, truncated: false, jobId: job.id,
|
|
238
|
+
modelRepo: job.model.repo, modelRevision: job.model.revision, runId, idempotencyKey } };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function destroyConfirmed(config, key, instanceId, signal) {
|
|
242
|
+
await api(config, key, 'DELETE', `/api/v0/instances/${instanceId}/`, { body: {}, signal });
|
|
243
|
+
for (let check = 0; check < 6; check++) {
|
|
244
|
+
if (!(await instances(config, key, signal)).some(row => String(row.id) === String(instanceId))) return true;
|
|
245
|
+
await new Promise(done => setTimeout(done, 2500));
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function cleanup({ job, runId, signal }) {
|
|
251
|
+
const config = await readConfig(), key = await apiKey(config, job.secretRef);
|
|
252
|
+
const released = await destroyConfirmed(config, key, runId, signal);
|
|
253
|
+
if (released) await run('systemctl', ['--user', 'stop', `ruvllm-vast-guard-${runId}.timer`], { timeoutMs: 5000 });
|
|
254
|
+
await writePrivate(join(config.resultsRoot, `cleanup-${runId}.json`), { schema: 'ruvllm-vast-cleanup/v1', jobId: job.id,
|
|
255
|
+
instanceId: runId, confirmedAbsent: released, at: new Date(now()).toISOString() });
|
|
256
|
+
return { released };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return { submit, inspect, cleanup, destroyConfirmed, instances };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const defaults = createVastAdapter();
|
|
263
|
+
export const submit = input => defaults.submit(input);
|
|
264
|
+
export const inspect = input => defaults.inspect(input);
|
|
265
|
+
export const cleanup = input => defaults.cleanup(input);
|
|
266
|
+
|
|
267
|
+
if (process.argv[1] === SELF && process.argv[2] === 'guard') {
|
|
268
|
+
const [instanceId, ref] = process.argv.slice(3);
|
|
269
|
+
const config = await readHostConfig();
|
|
270
|
+
const released = await defaults.destroyConfirmed(config, await apiKey(config, ref), instanceId);
|
|
271
|
+
await writePrivate(join(config.resultsRoot, `guard-${instanceId}.json`), { schema: 'ruvllm-vast-guard/v1', instanceId,
|
|
272
|
+
confirmedAbsent: released, at: new Date().toISOString() });
|
|
273
|
+
process.exitCode = released ? 0 : 1;
|
|
274
|
+
}
|