@ruvector/ruvllm 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +447 -160
- package/bin/cli.js +1066 -0
- package/bin/gpu-delegate.js +67 -0
- package/bin/ruvllm-adapters.js +5 -0
- package/bin/ruvllm-gpu-loop.js +5 -0
- package/bin/serve.js +265 -0
- package/dist/cjs/benchmarks/embedding-benchmark.d.ts +102 -0
- package/dist/cjs/benchmarks/embedding-benchmark.d.ts.map +1 -0
- package/dist/cjs/benchmarks/embedding-benchmark.js +436 -0
- package/dist/cjs/benchmarks/index.d.ts +34 -0
- package/dist/cjs/benchmarks/index.d.ts.map +1 -0
- package/dist/cjs/benchmarks/index.js +132 -0
- package/dist/cjs/benchmarks/model-comparison.d.ts +71 -0
- package/dist/cjs/benchmarks/model-comparison.d.ts.map +1 -0
- package/dist/cjs/benchmarks/model-comparison.js +476 -0
- package/dist/cjs/benchmarks/routing-benchmark.d.ts +70 -0
- package/dist/cjs/benchmarks/routing-benchmark.d.ts.map +1 -0
- package/dist/cjs/benchmarks/routing-benchmark.js +289 -0
- package/dist/cjs/contrastive.d.ts +229 -0
- package/dist/cjs/contrastive.d.ts.map +1 -0
- package/dist/cjs/contrastive.js +589 -0
- package/dist/cjs/engine.d.ts +160 -0
- package/dist/cjs/engine.d.ts.map +1 -0
- package/dist/cjs/engine.js +552 -0
- package/dist/cjs/export.d.ts +182 -0
- package/dist/cjs/export.d.ts.map +1 -0
- package/dist/cjs/export.js +422 -0
- package/dist/cjs/federated.d.ts +233 -0
- package/dist/cjs/federated.d.ts.map +1 -0
- package/dist/cjs/federated.js +525 -0
- package/dist/cjs/index.d.ts +68 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +104 -0
- package/dist/cjs/intelligence.d.ts +136 -0
- package/dist/cjs/intelligence.d.ts.map +1 -0
- package/dist/cjs/intelligence.js +226 -0
- package/dist/cjs/lora.d.ts +251 -0
- package/dist/cjs/lora.d.ts.map +1 -0
- package/dist/cjs/lora.js +509 -0
- package/dist/cjs/models.d.ts +129 -0
- package/dist/cjs/models.d.ts.map +1 -0
- package/dist/cjs/models.js +323 -0
- package/dist/cjs/native.d.ts +140 -0
- package/dist/cjs/native.d.ts.map +1 -0
- package/dist/cjs/native.js +98 -0
- package/dist/cjs/session.d.ts +80 -0
- package/dist/cjs/session.d.ts.map +1 -0
- package/dist/cjs/session.js +203 -0
- package/dist/cjs/simd.d.ts +90 -0
- package/dist/cjs/simd.d.ts.map +1 -0
- package/dist/cjs/simd.js +209 -0
- package/dist/cjs/sona.d.ts +223 -0
- package/dist/cjs/sona.d.ts.map +1 -0
- package/dist/cjs/sona.js +562 -0
- package/dist/cjs/streaming.d.ts +55 -0
- package/dist/cjs/streaming.d.ts.map +1 -0
- package/dist/cjs/streaming.js +131 -0
- package/dist/cjs/training.d.ts +312 -0
- package/dist/cjs/training.d.ts.map +1 -0
- package/dist/cjs/training.js +610 -0
- package/dist/cjs/types.d.ts +696 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js +6 -0
- package/dist/esm/benchmarks/embedding-benchmark.d.ts +102 -0
- package/dist/esm/benchmarks/embedding-benchmark.d.ts.map +1 -0
- package/dist/esm/benchmarks/embedding-benchmark.js +427 -0
- package/dist/esm/benchmarks/index.d.ts +34 -0
- package/dist/esm/benchmarks/index.d.ts.map +1 -0
- package/dist/esm/benchmarks/index.js +110 -0
- package/dist/esm/benchmarks/model-comparison.d.ts +71 -0
- package/dist/esm/benchmarks/model-comparison.d.ts.map +1 -0
- package/dist/esm/benchmarks/model-comparison.js +434 -0
- package/dist/esm/benchmarks/routing-benchmark.d.ts +70 -0
- package/dist/esm/benchmarks/routing-benchmark.d.ts.map +1 -0
- package/dist/esm/benchmarks/routing-benchmark.js +283 -0
- package/dist/esm/contrastive.d.ts +229 -0
- package/dist/esm/contrastive.d.ts.map +1 -0
- package/dist/esm/contrastive.js +578 -0
- package/dist/esm/engine.d.ts +160 -0
- package/dist/esm/engine.d.ts.map +1 -0
- package/dist/esm/engine.js +548 -0
- package/dist/esm/export.d.ts +182 -0
- package/dist/esm/export.d.ts.map +1 -0
- package/dist/esm/export.js +414 -0
- package/dist/esm/federated.d.ts +233 -0
- package/dist/esm/federated.d.ts.map +1 -0
- package/dist/esm/federated.js +520 -0
- package/dist/esm/index.d.ts +68 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +84 -0
- package/dist/esm/intelligence.d.ts +136 -0
- package/dist/esm/intelligence.d.ts.map +1 -0
- package/dist/esm/intelligence.js +188 -0
- package/dist/esm/lora.d.ts +251 -0
- package/dist/esm/lora.d.ts.map +1 -0
- package/dist/esm/lora.js +504 -0
- package/dist/esm/models.d.ts +129 -0
- package/dist/esm/models.d.ts.map +1 -0
- package/dist/esm/models.js +282 -0
- package/dist/esm/native.d.ts +140 -0
- package/dist/esm/native.d.ts.map +1 -0
- package/dist/esm/native.js +100 -0
- package/dist/esm/session.d.ts +80 -0
- package/dist/esm/session.d.ts.map +1 -0
- package/dist/esm/session.js +199 -0
- package/dist/esm/simd.d.ts +90 -0
- package/dist/esm/simd.d.ts.map +1 -0
- package/dist/esm/simd.js +205 -0
- package/dist/esm/sona.d.ts +223 -0
- package/dist/esm/sona.d.ts.map +1 -0
- package/dist/esm/sona.js +556 -0
- package/dist/esm/streaming.d.ts +55 -0
- package/dist/esm/streaming.d.ts.map +1 -0
- package/dist/esm/streaming.js +126 -0
- package/dist/esm/training.d.ts +312 -0
- package/dist/esm/training.d.ts.map +1 -0
- package/dist/esm/training.js +603 -0
- package/dist/esm/types.d.ts +696 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +5 -0
- package/gpu/_load.cjs +38 -0
- package/gpu/_load.mjs +33 -0
- package/gpu/adapters.cjs +3 -0
- package/gpu/adapters.mjs +5 -0
- package/gpu/engine.cjs +3 -0
- package/gpu/engine.mjs +5 -0
- package/gpu/fixture.cjs +3 -0
- package/gpu/fixture.mjs +5 -0
- package/gpu/gpu-host.cjs +3 -0
- package/gpu/gpu-host.mjs +5 -0
- package/gpu/gpu-loop.cjs +3 -0
- package/gpu/gpu-loop.mjs +5 -0
- package/gpu/gpu-spool-adapter.cjs +3 -0
- package/gpu/gpu-spool-adapter.mjs +5 -0
- package/gpu/vast-host-adapter.cjs +3 -0
- package/gpu/vast-host-adapter.mjs +5 -0
- package/package.json +159 -49
- package/scripts/ensemble-model-compare.js +287 -0
- package/scripts/fix-esm-extensions.mjs +220 -0
- package/scripts/huggingface/README.md +537 -0
- package/scripts/huggingface/publish.sh +112 -0
- package/scripts/hybrid-model-compare.js +373 -0
- package/scripts/improved-model-compare.js +288 -0
- package/scripts/optimized-model-compare.js +364 -0
- package/scripts/real-model-compare.js +280 -0
- package/scripts/smoke-esm.mjs +82 -0
- package/scripts/training/agentic-flow-capabilities.json +329 -0
- package/scripts/training/claude-code-synth.js +505 -0
- package/scripts/training/claude-flow-capabilities.json +1322 -0
- package/scripts/training/claude-hard-negatives.js +222 -0
- package/scripts/training/contrastive-finetune.js +517 -0
- package/scripts/training/ecosystem-triplets.jsonl +1467 -0
- package/scripts/training/generate-ecosystem-triplets.js +1324 -0
- package/scripts/training/routing-dataset.js +634 -0
- package/scripts/training/ruvector-capabilities.json +798 -0
- package/scripts/training/validate-ecosystem.js +381 -0
- package/scripts/training/validation-results.json +23 -0
- package/GPU-LOOP.md +0 -229
- package/LICENSE +0 -201
- package/NOTICE +0 -9
- package/examples/gpu-loop.example.json +0 -26
- package/src/adapters-cli.mjs +0 -23
- package/src/adapters.mjs +0 -91
- package/src/cli.mjs +0 -30
- package/src/engine.mjs +0 -238
- package/src/fixture.mjs +0 -38
- package/src/gpu-host.mjs +0 -47
- package/src/gpu-loop-cli.mjs +0 -67
- package/src/gpu-loop.mjs +0 -184
- package/src/gpu-spool-adapter.mjs +0 -80
- package/src/vast-host-adapter.mjs +0 -274
package/src/fixture.mjs
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { join, resolve } from 'node:path';
|
|
3
|
-
import { fileDigest, sha256, checkedPath } from './engine.mjs';
|
|
4
|
-
|
|
5
|
-
// This deterministic fixture measures contracts and native I/O, never model quality.
|
|
6
|
-
export async function createFixture({ directory, binaryPath }) {
|
|
7
|
-
directory = resolve(directory); binaryPath = await checkedPath(resolve(binaryPath));
|
|
8
|
-
await mkdir(directory, { mode: 0o700 });
|
|
9
|
-
const inputs = join(directory, 'inputs'), outputs = join(directory, 'outputs');
|
|
10
|
-
await mkdir(inputs, { mode: 0o700 }); await mkdir(outputs, { mode: 0o700 });
|
|
11
|
-
async function json(name, value) {
|
|
12
|
-
const path = join(inputs, name), bytes = Buffer.from(JSON.stringify(value, null, 2) + '\n');
|
|
13
|
-
await writeFile(path, bytes, { flag: 'wx', mode: 0o600 }); return { path, sha256: sha256(bytes) };
|
|
14
|
-
}
|
|
15
|
-
const rawHeader = JSON.stringify({ 'layer.weight': { dtype: 'BF16', shape: [4], data_offsets: [0, 8] } });
|
|
16
|
-
const header = Buffer.from(rawHeader + ' '.repeat((8 - Buffer.byteLength(rawHeader) % 8) % 8));
|
|
17
|
-
const size = Buffer.alloc(8); size.writeBigUInt64LE(BigInt(header.length));
|
|
18
|
-
const values = Buffer.alloc(8); [0x3f80, 0x4000, 0x4040, 0x4080].forEach((value, i) => values.writeUInt16LE(value, i * 2));
|
|
19
|
-
const original = Buffer.concat([size, header, values]), source = join(inputs, 'original.safetensors');
|
|
20
|
-
await writeFile(source, original, { flag: 'wx', mode: 0o600 });
|
|
21
|
-
const candidate = Buffer.from(original); candidate.writeUInt16LE(0x4020, candidate.length - 6);
|
|
22
|
-
const policy = await json('policy.json', { allowed_tensors: ['layer.weight'], max_changed_values: 1, max_l2: 0.5, max_abs_delta: 0.5, max_relative_l2: 1, protected: [] });
|
|
23
|
-
const patch = await json('patch.json', { schema: 'ruvllm.weight_patch.v1', source_sha256: sha256(original), edits: [{ tensor: 'layer.weight', index: 1, old_bits: 0x4000, new_bits: 0x4020 }] });
|
|
24
|
-
const binding = { runtime_sha256: await fileDigest(binaryPath), topology_sha256: '2'.repeat(64), protocol_sha256: '3'.repeat(64), toolchain_sha256: '4'.repeat(64) };
|
|
25
|
-
const evaluation = await json('fixture-evaluation.json', {
|
|
26
|
-
schema: 'ruvllm.weight_evaluation.v1',
|
|
27
|
-
baseline: { ...binding, model_generation: 0, model_sha256: sha256(original), patch_sha256: '0'.repeat(64) },
|
|
28
|
-
candidate: { ...binding, model_generation: 1, model_sha256: sha256(candidate), patch_sha256: patch.sha256 },
|
|
29
|
-
suite_sha256: '5'.repeat(64), protocol_sha256: binding.protocol_sha256,
|
|
30
|
-
baseline_output_sha256: '6'.repeat(64), candidate_output_sha256: '7'.repeat(64),
|
|
31
|
-
evaluator_identity: 'synthetic-fixture-evaluator', proposer_identity: 'synthetic-fixture-proposer',
|
|
32
|
-
total: 2, baseline_correct: 0, candidate_correct: 1, baseline_truncated: 1, candidate_truncated: 0,
|
|
33
|
-
});
|
|
34
|
-
const plan = await json('plan.json', { schema: 'ruvllm-experiment-plan/v1', evidenceMode: 'fixture', source, policy: policy.path, candidates: [{ id: 'fixture-improvement', patch: patch.path, evaluation: evaluation.path }, { id: 'unevaluated-control', patch: patch.path }] });
|
|
35
|
-
const configPath = join(directory, 'runtime.json');
|
|
36
|
-
await writeFile(configPath, JSON.stringify({ schema: 'ruvllm-experiment-runtime/v1', binaryPath, binarySha256: binding.runtime_sha256, readRoots: [inputs], writeRoot: outputs, policySha256s: [policy.sha256], maxCandidates: 16, timeoutMs: 30000 }, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
37
|
-
return { configPath, planPath: plan.path };
|
|
38
|
-
}
|
package/src/gpu-host.mjs
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// The operator supplies this module locally. Secret resolution and provider APIs
|
|
2
|
-
// remain with that host; this loader never fetches credentials or parses stdout.
|
|
3
|
-
import { createHash } from 'node:crypto';
|
|
4
|
-
import { constants } from 'node:fs';
|
|
5
|
-
import { open } from 'node:fs/promises';
|
|
6
|
-
import { pathToFileURL } from 'node:url';
|
|
7
|
-
import { checkedPath } from './engine.mjs';
|
|
8
|
-
|
|
9
|
-
function failure(code) { const error = new Error(code); error.code = code; return error; }
|
|
10
|
-
|
|
11
|
-
export async function loadHostAdapter({ modulePath, sha256 }) {
|
|
12
|
-
if (typeof sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(sha256)) throw failure('HOST_ADAPTER_INVALID_PIN');
|
|
13
|
-
let path, bytes;
|
|
14
|
-
try {
|
|
15
|
-
path = await checkedPath(modulePath);
|
|
16
|
-
if (!path.endsWith('.mjs')) throw failure('HOST_ADAPTER_INVALID_PATH');
|
|
17
|
-
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
18
|
-
try {
|
|
19
|
-
const stat = await file.stat();
|
|
20
|
-
if (!stat.isFile() || stat.size > 1024 * 1024 || (stat.mode & 0o077) !== 0
|
|
21
|
-
|| typeof process.getuid !== 'function' || stat.uid !== process.getuid()) {
|
|
22
|
-
throw failure('HOST_ADAPTER_UNSAFE_FILE');
|
|
23
|
-
}
|
|
24
|
-
bytes = Buffer.alloc(stat.size);
|
|
25
|
-
const { bytesRead } = await file.read(bytes, 0, bytes.length, 0);
|
|
26
|
-
if (bytesRead !== bytes.length) throw failure('HOST_ADAPTER_CHANGED');
|
|
27
|
-
} finally { await file.close(); }
|
|
28
|
-
} catch (error) {
|
|
29
|
-
if (error.code?.startsWith('HOST_ADAPTER_')) throw error;
|
|
30
|
-
throw failure('HOST_ADAPTER_INVALID_PATH');
|
|
31
|
-
}
|
|
32
|
-
if (createHash('sha256').update(bytes).digest('hex') !== sha256) throw failure('HOST_ADAPTER_PIN_MISMATCH');
|
|
33
|
-
let module;
|
|
34
|
-
try {
|
|
35
|
-
// The entry point and its transitive imports are trusted operator code. The
|
|
36
|
-
// digest is a cache identity as well as a pin; never accept tool-supplied code.
|
|
37
|
-
const url = pathToFileURL(path); url.searchParams.set('sha256', sha256);
|
|
38
|
-
module = await import(url.href);
|
|
39
|
-
} catch { throw failure('HOST_ADAPTER_UNAVAILABLE'); }
|
|
40
|
-
if (!['submit', 'inspect', 'cleanup'].every(name => typeof module[name] === 'function')) {
|
|
41
|
-
throw failure('HOST_ADAPTER_INVALID_CONTRACT');
|
|
42
|
-
}
|
|
43
|
-
return Object.freeze(Object.fromEntries(['submit', 'inspect', 'cleanup'].map(name => [name, async input => {
|
|
44
|
-
try { return await module[name](input); }
|
|
45
|
-
catch { throw failure('HOST_ADAPTER_OPERATION_FAILED'); }
|
|
46
|
-
}])));
|
|
47
|
-
}
|
package/src/gpu-loop-cli.mjs
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { setTimeout as wait } from 'node:timers/promises';
|
|
3
|
-
import { tickLoop, readLoopStatus, readLoopConfig } from './gpu-loop.mjs';
|
|
4
|
-
import { loadHostAdapter } from './gpu-host.mjs';
|
|
5
|
-
|
|
6
|
-
const usage = 'Usage: node src/gpu-loop-cli.mjs tick|status|run --config ABS\n'
|
|
7
|
-
+ ' tick/run require --adapter ABS.mjs --adapter-sha256 SHA256\n'
|
|
8
|
-
+ ' run accepts --max-ticks 1..10000 (default 1); no infinite loop is started.\n'
|
|
9
|
-
+ 'The trusted host resolves ruOS secret references and executes Vast.ai operations.\n'
|
|
10
|
-
+ 'No provider or secret access is supplied by this CLI.\n';
|
|
11
|
-
|
|
12
|
-
function options(argv, command) {
|
|
13
|
-
const result = {};
|
|
14
|
-
const allowed = command === 'status' ? ['--config']
|
|
15
|
-
: ['--config', '--adapter', '--adapter-sha256', ...(command === 'run' ? ['--max-ticks'] : [])];
|
|
16
|
-
for (let index = 0; index < argv.length; index += 2) {
|
|
17
|
-
const key = argv[index], value = argv[index + 1];
|
|
18
|
-
if (!allowed.includes(key) || !value || value.startsWith('--') || Object.hasOwn(result, key)) throw new Error('INVALID_ARGUMENTS');
|
|
19
|
-
result[key] = value;
|
|
20
|
-
}
|
|
21
|
-
if (!result['--config'] || (command !== 'status' && (!result['--adapter'] || !result['--adapter-sha256']))) throw new Error('INVALID_ARGUMENTS');
|
|
22
|
-
const maxTicks = Number(result['--max-ticks'] ?? 1);
|
|
23
|
-
if (!Number.isSafeInteger(maxTicks) || maxTicks < 1 || maxTicks > 10000) throw new Error('INVALID_ARGUMENTS');
|
|
24
|
-
return { ...result, maxTicks };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const [command, ...argv] = process.argv.slice(2);
|
|
28
|
-
if (command === '--help' || command === 'help') process.stdout.write(usage);
|
|
29
|
-
else if (!['tick', 'status', 'run'].includes(command)) {
|
|
30
|
-
process.stderr.write(usage); process.exitCode = 2;
|
|
31
|
-
} else {
|
|
32
|
-
const stop = new AbortController();
|
|
33
|
-
const stopLoop = () => stop.abort();
|
|
34
|
-
process.once('SIGINT', stopLoop); process.once('SIGTERM', stopLoop);
|
|
35
|
-
try {
|
|
36
|
-
const flags = options(argv, command);
|
|
37
|
-
const configPath = flags['--config'];
|
|
38
|
-
if (command === 'status') process.stdout.write(JSON.stringify(await readLoopStatus({ configPath })) + '\n');
|
|
39
|
-
else {
|
|
40
|
-
const ticks = command === 'run' ? flags.maxTicks : 1;
|
|
41
|
-
for (let index = 0; index < ticks && !stop.signal.aborted; index++) {
|
|
42
|
-
// Revalidate code and immutable config before each tick. Tick locks and
|
|
43
|
-
// durable submission markers own duplicate prevention across processes.
|
|
44
|
-
const config = await readLoopConfig({ configPath });
|
|
45
|
-
const adapter = await loadHostAdapter({ modulePath: flags['--adapter'], sha256: flags['--adapter-sha256'] });
|
|
46
|
-
const state = await tickLoop({ configPath, adapter });
|
|
47
|
-
process.stdout.write(JSON.stringify(state) + '\n');
|
|
48
|
-
if (index + 1 < ticks && !stop.signal.aborted) {
|
|
49
|
-
// The scheduler may shorten nextTickAt to a job deadline or immediate
|
|
50
|
-
// cleanup. Never sleep the full cadence across those boundaries.
|
|
51
|
-
const delayMs = Math.max(10, Math.min(config.intervalSeconds * 1000, state.nextTickAt - Date.now()));
|
|
52
|
-
try { await wait(delayMs, undefined, { signal: stop.signal }); }
|
|
53
|
-
catch (error) { if (error.name !== 'AbortError') throw error; }
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
} catch (error) {
|
|
58
|
-
// Raw host exceptions can carry credentials. Surface only scheduler-owned
|
|
59
|
-
// fixed codes; detailed provider diagnostics remain with the host.
|
|
60
|
-
const code = typeof error.code === 'string' && /^[A-Z][A-Z0-9_]{0,63}$/.test(error.code)
|
|
61
|
-
? error.code : 'GPU_LOOP_FAILED';
|
|
62
|
-
process.stderr.write(JSON.stringify({ ok: false, code, productionActivation: false }) + '\n');
|
|
63
|
-
process.exitCode = 1;
|
|
64
|
-
} finally {
|
|
65
|
-
process.removeListener('SIGINT', stopLoop); process.removeListener('SIGTERM', stopLoop);
|
|
66
|
-
}
|
|
67
|
-
}
|
package/src/gpu-loop.mjs
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
// Persistent orchestration only. A trusted host adapter owns provider APIs and secret resolution.
|
|
2
|
-
import { randomUUID, createHash } from 'node:crypto';
|
|
3
|
-
import { constants } from 'node:fs';
|
|
4
|
-
import { open, lstat, mkdir, rename, rm } from 'node:fs/promises';
|
|
5
|
-
import { join, isAbsolute, resolve } from 'node:path';
|
|
6
|
-
import { checkedPath, parseJson } from './engine.mjs';
|
|
7
|
-
|
|
8
|
-
const fail = (ok, message) => { if (!ok) throw new Error(message); };
|
|
9
|
-
const id = value => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/.test(value);
|
|
10
|
-
const sha = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
11
|
-
const amount = value => typeof value === 'number' && Number.isFinite(value) && value >= 0
|
|
12
|
-
&& Number.isSafeInteger(Math.round(value * 1e6)) && Math.round(value * 1e6) / 1e6 === value;
|
|
13
|
-
const exact = (value, required, optional = []) => fail(value && typeof value === 'object' && !Array.isArray(value)
|
|
14
|
-
&& required.every(key => Object.hasOwn(value, key)) && Object.keys(value).every(key => [...required, ...optional].includes(key)), 'Invalid or unknown fields');
|
|
15
|
-
const privateOwner = stat => fail((stat.mode & 0o077) === 0 && stat.uid === process.getuid(), 'Owner-only paths required');
|
|
16
|
-
const freeze = value => {
|
|
17
|
-
if (value && typeof value === 'object') {
|
|
18
|
-
for (const nested of Object.values(value)) freeze(nested);
|
|
19
|
-
Object.freeze(value);
|
|
20
|
-
}
|
|
21
|
-
return value;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
async function readJson(path) {
|
|
25
|
-
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
26
|
-
try {
|
|
27
|
-
const stat = await handle.stat(); privateOwner(stat);
|
|
28
|
-
fail(stat.isFile() && stat.size <= 1024 * 1024, 'Bounded regular JSON file required');
|
|
29
|
-
const bytes = await handle.readFile(); fail(bytes.length === stat.size, 'JSON file changed');
|
|
30
|
-
return { value: parseJson(bytes), digest: sha(bytes) };
|
|
31
|
-
} finally { await handle.close(); }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function readLoopConfig({ configPath }) {
|
|
35
|
-
await checkedPath(configPath);
|
|
36
|
-
const { value: config, digest } = await readJson(configPath);
|
|
37
|
-
exact(config, ['schema', 'stateRoot', 'intervalSeconds', 'maxAttempts', 'budgetUsd', 'jobs']);
|
|
38
|
-
fail(config.schema === 'ruvllm-gpu-loop/v1', 'Unsupported loop config');
|
|
39
|
-
fail(Number.isInteger(config.intervalSeconds) && config.intervalSeconds >= 1 && config.intervalSeconds <= 86400, 'Invalid interval');
|
|
40
|
-
fail(Number.isInteger(config.maxAttempts) && config.maxAttempts >= 1 && config.maxAttempts <= 10, 'Invalid attempts');
|
|
41
|
-
fail(amount(config.budgetUsd), 'Invalid loop budget');
|
|
42
|
-
await checkedPath(config.stateRoot, { directory: true }); privateOwner(await lstat(config.stateRoot));
|
|
43
|
-
fail(configPath !== join(config.stateRoot, 'state.json') && configPath !== join(config.stateRoot, 'tick.lock'), 'Config collides with scheduler state');
|
|
44
|
-
fail(Array.isArray(config.jobs) && config.jobs.length >= 1 && config.jobs.length <= 128, 'Expected 1..128 jobs');
|
|
45
|
-
const seen = new Set();
|
|
46
|
-
for (const job of config.jobs) {
|
|
47
|
-
exact(job, ['id', 'provider', 'model', 'task', 'secretRef', 'resource', 'maxCostUsd', 'timeoutSeconds'], ['experiment']);
|
|
48
|
-
fail(id(job.id) && !seen.has(job.id), 'Invalid or duplicate job ID'); seen.add(job.id);
|
|
49
|
-
fail(job.provider === 'vast.ai' && ['evaluate', 'train'].includes(job.task), 'Unsupported provider/task');
|
|
50
|
-
exact(job.model, ['repo', 'revision']);
|
|
51
|
-
fail(typeof job.model.repo === 'string' && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(job.model.repo)
|
|
52
|
-
&& /^[a-f0-9]{40}$/.test(job.model.revision), 'Pinned model revision required');
|
|
53
|
-
fail(id(job.secretRef), 'Opaque host secret reference required');
|
|
54
|
-
exact(job.resource, [], ['instanceId', 'offerId']);
|
|
55
|
-
fail(Object.keys(job.resource).length === 1 && Object.values(job.resource).every(id), 'Exactly one instanceId or offerId required');
|
|
56
|
-
fail(amount(job.maxCostUsd) && job.maxCostUsd > 0, 'Invalid job cost ceiling');
|
|
57
|
-
fail(Number.isInteger(job.timeoutSeconds) && job.timeoutSeconds >= 1 && job.timeoutSeconds <= 86400, 'Invalid job timeout');
|
|
58
|
-
if (job.experiment) {
|
|
59
|
-
exact(job.experiment, ['configPath', 'planPath']);
|
|
60
|
-
fail(Object.values(job.experiment).every(path => typeof path === 'string' && isAbsolute(path) && path === resolve(path)), 'Absolute experiment paths required');
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return freeze({ ...config, configSha256: digest });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function initial(config) {
|
|
67
|
-
return { schema: 'ruvllm-gpu-loop-state/v1', configSha256: config.configSha256,
|
|
68
|
-
nextTickAt: 0, reservedUsd: 0, jobs: config.jobs.map(job => ({ id: job.id, status: 'queued', attempts: 0 })), productionActivation: false };
|
|
69
|
-
}
|
|
70
|
-
async function load(config) {
|
|
71
|
-
const path = join(config.stateRoot, 'state.json');
|
|
72
|
-
let state;
|
|
73
|
-
try { state = (await readJson(path)).value; }
|
|
74
|
-
catch (error) { if (error.code === 'ENOENT') return initial(config); throw error; }
|
|
75
|
-
fail(state.schema === 'ruvllm-gpu-loop-state/v1' && state.configSha256 === config.configSha256, 'State/config binding mismatch');
|
|
76
|
-
fail(Array.isArray(state.jobs) && state.jobs.length === config.jobs.length && state.jobs.every((job, index) =>
|
|
77
|
-
job.id === config.jobs[index].id && ['queued', 'submitting', 'running', 'cleanup', 'succeeded', 'failed', 'blocked'].includes(job.status)
|
|
78
|
-
&& Number.isInteger(job.attempts) && job.attempts >= 0 && job.attempts <= config.maxAttempts), 'Invalid state records');
|
|
79
|
-
fail(amount(state.reservedUsd) && state.reservedUsd <= config.budgetUsd
|
|
80
|
-
&& Number.isSafeInteger(state.nextTickAt) && state.nextTickAt >= 0 && state.productionActivation === false, 'Invalid state budget/timing');
|
|
81
|
-
fail(Math.round(state.reservedUsd * 1e6) === state.jobs.reduce((sum, entry, index) =>
|
|
82
|
-
sum + entry.attempts * Math.round(config.jobs[index].maxCostUsd * 1e6), 0), 'State reservation ledger mismatch');
|
|
83
|
-
for (const entry of state.jobs) {
|
|
84
|
-
if (['submitting', 'running', 'cleanup', 'succeeded', 'blocked'].includes(entry.status)) {
|
|
85
|
-
fail(entry.attempts > 0 && id(entry.idempotencyKey) && Number.isSafeInteger(entry.startedAt) && entry.startedAt >= 0, 'Missing attempt identity');
|
|
86
|
-
}
|
|
87
|
-
if (['running', 'cleanup', 'succeeded'].includes(entry.status)) fail(id(entry.runId), 'Missing provider run identity');
|
|
88
|
-
if (entry.status === 'cleanup') fail(['succeeded', 'failed'].includes(entry.targetState) && typeof entry.retryable === 'boolean', 'Invalid cleanup intent');
|
|
89
|
-
if (entry.status === 'succeeded') fail(entry.cleanupConfirmed === true, 'Completion requires cleanup');
|
|
90
|
-
}
|
|
91
|
-
return state;
|
|
92
|
-
}
|
|
93
|
-
async function persist(config, state) {
|
|
94
|
-
const temporary = join(config.stateRoot, `.state-${randomUUID()}.json`);
|
|
95
|
-
const handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
96
|
-
try { await handle.writeFile(JSON.stringify(state, null, 2) + '\n'); await handle.sync(); } finally { await handle.close(); }
|
|
97
|
-
await rename(temporary, join(config.stateRoot, 'state.json'));
|
|
98
|
-
const directory = await open(config.stateRoot, constants.O_RDONLY); try { await directory.sync(); } finally { await directory.close(); }
|
|
99
|
-
}
|
|
100
|
-
async function call(method, input, milliseconds) {
|
|
101
|
-
const controller = new AbortController(); let timer;
|
|
102
|
-
try {
|
|
103
|
-
return await Promise.race([Promise.resolve().then(() => method({ ...input, signal: controller.signal })),
|
|
104
|
-
new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(); reject(new Error('ADAPTER_TIMEOUT')); }, milliseconds); })]);
|
|
105
|
-
} finally { clearTimeout(timer); }
|
|
106
|
-
}
|
|
107
|
-
function verified(completion, job, entry) {
|
|
108
|
-
return completion && completion.completionVersion === 1 && completion.completionVerified === true
|
|
109
|
-
&& completion.status === 'ok' && completion.exitCode === 0 && completion.assertionsVerified === true
|
|
110
|
-
&& completion.evidenceComplete === true && completion.truncated === false
|
|
111
|
-
&& completion.jobId === job.id && completion.modelRevision === job.model.revision
|
|
112
|
-
&& completion.modelRepo === job.model.repo && completion.runId === entry.runId
|
|
113
|
-
&& completion.idempotencyKey === entry.idempotencyKey;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export async function readLoopStatus({ configPath }) { return load(await readLoopConfig({ configPath })); }
|
|
117
|
-
|
|
118
|
-
export async function tickLoop({ configPath, adapter, now = Date.now() }) {
|
|
119
|
-
fail(Number.isSafeInteger(now) && now >= 0, 'Invalid clock');
|
|
120
|
-
fail(adapter && ['submit', 'inspect', 'cleanup'].every(method => typeof adapter[method] === 'function'), 'Host adapter required');
|
|
121
|
-
const config = await readLoopConfig({ configPath }), lock = join(config.stateRoot, 'tick.lock');
|
|
122
|
-
await mkdir(lock, { mode: 0o700 }); // Never break an existing lock: operator reconciliation is required after a crash.
|
|
123
|
-
try {
|
|
124
|
-
const state = await load(config);
|
|
125
|
-
const deadlineDue = state.jobs.some(entry => entry.status === 'running'
|
|
126
|
-
&& now >= entry.startedAt + config.jobs.find(job => job.id === entry.id).timeoutSeconds * 1000);
|
|
127
|
-
if (state.jobs.some(job => job.status === 'blocked') || (now < state.nextTickAt && !deadlineDue)) return state;
|
|
128
|
-
state.nextTickAt = now + config.intervalSeconds * 1000;
|
|
129
|
-
const entry = state.jobs.find(job => ['submitting', 'running', 'cleanup'].includes(job.status))
|
|
130
|
-
?? state.jobs.find(job => job.status === 'queued');
|
|
131
|
-
if (!entry) { await persist(config, state); return state; }
|
|
132
|
-
const job = config.jobs.find(job => job.id === entry.id);
|
|
133
|
-
if (entry.status === 'submitting') { entry.status = 'blocked'; entry.reason = 'ambiguous_submission_reconcile'; }
|
|
134
|
-
else if (entry.status === 'queued') {
|
|
135
|
-
const reserved = Math.round(state.reservedUsd * 1e6) + Math.round(job.maxCostUsd * 1e6);
|
|
136
|
-
if (entry.attempts >= config.maxAttempts || reserved > Math.round(config.budgetUsd * 1e6)) {
|
|
137
|
-
entry.status = 'failed'; entry.reason = 'attempt_or_budget_limit';
|
|
138
|
-
} else {
|
|
139
|
-
entry.attempts++; state.reservedUsd = reserved / 1e6;
|
|
140
|
-
for (const key of ['runId', 'cleanupConfirmed', 'targetState', 'retryable', 'completionSha256']) delete entry[key];
|
|
141
|
-
entry.status = 'submitting'; entry.idempotencyKey = randomUUID(); entry.startedAt = now;
|
|
142
|
-
await persist(config, state); // Persist intent and reservation before the provider effect.
|
|
143
|
-
try {
|
|
144
|
-
const submitted = await call(adapter.submit.bind(adapter), { job, idempotencyKey: entry.idempotencyKey }, Math.min(30000, job.timeoutSeconds * 1000));
|
|
145
|
-
fail(submitted && id(submitted.runId), 'Invalid provider run ID');
|
|
146
|
-
entry.runId = submitted.runId; entry.status = 'running'; entry.reason = 'submitted';
|
|
147
|
-
} catch { entry.status = 'blocked'; entry.reason = 'ambiguous_submission_reconcile'; }
|
|
148
|
-
}
|
|
149
|
-
} else if (entry.status === 'running') {
|
|
150
|
-
let result;
|
|
151
|
-
if (now >= entry.startedAt + job.timeoutSeconds * 1000) {
|
|
152
|
-
entry.status = 'cleanup'; entry.targetState = 'failed'; entry.retryable = false; entry.reason = 'job_deadline';
|
|
153
|
-
} else {
|
|
154
|
-
try {
|
|
155
|
-
result = await call(adapter.inspect.bind(adapter), { job, runId: entry.runId, idempotencyKey: entry.idempotencyKey }, Math.min(30000, entry.startedAt + job.timeoutSeconds * 1000 - now));
|
|
156
|
-
fail(result && ['running', 'completed', 'failed'].includes(result.status), 'Invalid provider inspection');
|
|
157
|
-
if (result.costUsd !== undefined) fail(amount(result.costUsd) && result.costUsd <= job.maxCostUsd, 'Provider cost breach');
|
|
158
|
-
if (result.status !== 'running') {
|
|
159
|
-
entry.status = 'cleanup'; entry.targetState = result.status === 'completed' && verified(result.completion, job, entry) ? 'succeeded' : 'failed';
|
|
160
|
-
entry.retryable = result.status === 'failed'; entry.reason = entry.targetState === 'succeeded' ? 'verified_completion' : result.status === 'failed' ? 'provider_failed' : 'unverified_completion';
|
|
161
|
-
if (entry.targetState === 'succeeded') entry.completionSha256 = sha(JSON.stringify(result.completion));
|
|
162
|
-
}
|
|
163
|
-
} catch { entry.status = 'cleanup'; entry.targetState = 'failed'; entry.retryable = false; entry.reason = 'inspection_failed_or_invalid'; }
|
|
164
|
-
}
|
|
165
|
-
} else if (entry.status === 'cleanup') {
|
|
166
|
-
await persist(config, state); // Cleanup intent is retained until resource absence is confirmed by the host.
|
|
167
|
-
try {
|
|
168
|
-
const result = await call(adapter.cleanup.bind(adapter), { job, runId: entry.runId }, 30000);
|
|
169
|
-
if (result?.released === true) {
|
|
170
|
-
entry.history ??= [];
|
|
171
|
-
entry.history.push({ attempt: entry.attempts, runId: entry.runId, idempotencyKey: entry.idempotencyKey,
|
|
172
|
-
outcome: entry.targetState, reason: entry.reason, cleanupConfirmed: true,
|
|
173
|
-
...(entry.completionSha256 ? { completionSha256: entry.completionSha256 } : {}) });
|
|
174
|
-
entry.status = entry.targetState;
|
|
175
|
-
if (entry.status === 'failed' && entry.retryable && entry.attempts < config.maxAttempts) entry.status = 'queued';
|
|
176
|
-
entry.cleanupConfirmed = true;
|
|
177
|
-
} else entry.reason = 'cleanup_unconfirmed';
|
|
178
|
-
} catch { entry.reason = 'cleanup_unconfirmed'; }
|
|
179
|
-
}
|
|
180
|
-
if (entry.status === 'running') state.nextTickAt = Math.min(state.nextTickAt, entry.startedAt + job.timeoutSeconds * 1000);
|
|
181
|
-
if (entry.status === 'cleanup' && entry.reason !== 'cleanup_unconfirmed') state.nextTickAt = now;
|
|
182
|
-
await persist(config, state); return state;
|
|
183
|
-
} finally { await rm(lock, { recursive: true }); }
|
|
184
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
// Optional host handoff: writes job requests, never contacts a GPU provider or resolves secrets.
|
|
2
|
-
import { constants } from 'node:fs';
|
|
3
|
-
import { open, lstat, realpath, writeFile } from 'node:fs/promises';
|
|
4
|
-
import { join, resolve, isAbsolute } from 'node:path';
|
|
5
|
-
import { parseJson } from './engine.mjs';
|
|
6
|
-
|
|
7
|
-
const ensure = (test, message) => { if (!test) throw new Error(message); };
|
|
8
|
-
const safeId = value => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,159}$/.test(value);
|
|
9
|
-
|
|
10
|
-
async function spoolRoot() {
|
|
11
|
-
const root = process.env.RUVLLM_GPU_SPOOL_ROOT;
|
|
12
|
-
ensure(typeof root === 'string' && isAbsolute(root) && resolve(root) === root, 'SPOOL_NOT_CONFIGURED');
|
|
13
|
-
ensure(await realpath(root) === root, 'SPOOL_PATH_NOT_CANONICAL');
|
|
14
|
-
const stat = await lstat(root);
|
|
15
|
-
ensure(stat.isDirectory() && !stat.isSymbolicLink() && (stat.mode & 0o077) === 0
|
|
16
|
-
&& stat.uid === process.getuid(), 'SPOOL_NOT_PRIVATE');
|
|
17
|
-
return root;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function writeOnce(path, value) {
|
|
21
|
-
const text = JSON.stringify(value, null, 2) + '\n';
|
|
22
|
-
ensure(Buffer.byteLength(text) <= 65536, 'SPOOL_REQUEST_TOO_LARGE');
|
|
23
|
-
try { await writeFile(path, text, { flag: 'wx', mode: 0o600 }); }
|
|
24
|
-
catch (error) {
|
|
25
|
-
if (error.code !== 'EEXIST') throw new Error('SPOOL_WRITE_FAILED');
|
|
26
|
-
const prior = await readReply(path);
|
|
27
|
-
ensure(JSON.stringify(prior) === JSON.stringify(value), 'SPOOL_IDENTITY_CONFLICT');
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function readReply(path) {
|
|
32
|
-
let handle;
|
|
33
|
-
try { handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); }
|
|
34
|
-
catch (error) { if (error.code === 'ENOENT') return null; throw new Error('SPOOL_READ_FAILED'); }
|
|
35
|
-
try {
|
|
36
|
-
const stat = await handle.stat();
|
|
37
|
-
ensure(stat.isFile() && stat.size <= 65536 && (stat.mode & 0o077) === 0
|
|
38
|
-
&& stat.uid === process.getuid(), 'SPOOL_REPLY_NOT_PRIVATE');
|
|
39
|
-
const bytes = Buffer.alloc(stat.size);
|
|
40
|
-
const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
|
|
41
|
-
ensure(bytesRead === bytes.length, 'SPOOL_REPLY_CHANGED');
|
|
42
|
-
return parseJson(bytes);
|
|
43
|
-
} finally { await handle.close(); }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function submit({ job, idempotencyKey, signal }) {
|
|
47
|
-
ensure(!signal?.aborted && safeId(idempotencyKey), 'SPOOL_INVALID_REQUEST');
|
|
48
|
-
const root = await spoolRoot();
|
|
49
|
-
await writeOnce(join(root, `${idempotencyKey}.request.json`), {
|
|
50
|
-
schema: 'ruvllm-gpu-spool-request/v1', runId: idempotencyKey, job,
|
|
51
|
-
credentialHandling: 'host-resolves-secretRef-no-secret-values-in-spool',
|
|
52
|
-
});
|
|
53
|
-
return { runId: idempotencyKey };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export async function inspect({ runId, signal }) {
|
|
57
|
-
ensure(!signal?.aborted && safeId(runId), 'SPOOL_INVALID_RUN_ID');
|
|
58
|
-
const root = await spoolRoot();
|
|
59
|
-
const reply = await readReply(join(root, `${runId}.status.json`));
|
|
60
|
-
if (!reply) return { status: 'running' };
|
|
61
|
-
ensure(reply.schema === 'ruvllm-gpu-spool-status/v1' && reply.runId === runId
|
|
62
|
-
&& ['running', 'completed', 'failed'].includes(reply.status)
|
|
63
|
-
&& Object.keys(reply).every(key => ['schema', 'runId', 'status', 'costUsd', 'completion'].includes(key)), 'SPOOL_INVALID_STATUS');
|
|
64
|
-
return { status: reply.status, ...(reply.costUsd === undefined ? {} : { costUsd: reply.costUsd }),
|
|
65
|
-
...(reply.completion === undefined ? {} : { completion: reply.completion }) };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function cleanup({ runId, job, signal }) {
|
|
69
|
-
ensure(!signal?.aborted && safeId(runId), 'SPOOL_INVALID_RUN_ID');
|
|
70
|
-
const root = await spoolRoot();
|
|
71
|
-
await writeOnce(join(root, `${runId}.cleanup-request.json`), {
|
|
72
|
-
schema: 'ruvllm-gpu-spool-cleanup-request/v1', runId, jobId: job.id,
|
|
73
|
-
});
|
|
74
|
-
const reply = await readReply(join(root, `${runId}.cleanup.json`));
|
|
75
|
-
if (!reply) return { released: false };
|
|
76
|
-
ensure(reply.schema === 'ruvllm-gpu-spool-cleanup/v1' && reply.runId === runId
|
|
77
|
-
&& typeof reply.released === 'boolean'
|
|
78
|
-
&& Object.keys(reply).every(key => ['schema', 'runId', 'released'].includes(key)), 'SPOOL_INVALID_CLEANUP');
|
|
79
|
-
return { released: reply.released };
|
|
80
|
-
}
|