@simulatte/doppler 0.1.5 → 0.1.6
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 +23 -8
- package/package.json +7 -4
- package/src/config/kernels/kernel-ref-digests.js +39 -39
- package/src/config/kernels/registry.json +42 -2
- package/src/config/loader.js +31 -2
- package/src/config/merge.js +18 -0
- package/src/config/presets/models/qwen3.json +9 -2
- package/src/config/presets/models/transformer.json +5 -0
- package/src/config/required-inference-fields-contract-check.js +6 -0
- package/src/config/schema/inference-defaults.schema.js +3 -0
- package/src/config/schema/inference.schema.d.ts +9 -0
- package/src/config/schema/kernel-path.schema.d.ts +6 -0
- package/src/config/schema/manifest.schema.d.ts +6 -0
- package/src/config/schema/manifest.schema.js +3 -0
- package/src/converter/rope-config.js +42 -0
- package/src/gpu/device.js +58 -0
- package/src/gpu/kernels/attention.js +98 -0
- package/src/gpu/kernels/bias_add.wgsl +8 -6
- package/src/gpu/kernels/bias_add_f16.wgsl +8 -5
- package/src/gpu/kernels/conv2d.js +1 -1
- package/src/gpu/kernels/conv2d.wgsl +7 -8
- package/src/gpu/kernels/conv2d_f16.wgsl +7 -8
- package/src/gpu/kernels/depthwise_conv2d.js +2 -1
- package/src/gpu/kernels/depthwise_conv2d.wgsl +6 -9
- package/src/gpu/kernels/depthwise_conv2d_f16.wgsl +6 -9
- package/src/gpu/kernels/grouped_pointwise_conv2d.js +2 -1
- package/src/gpu/kernels/grouped_pointwise_conv2d.wgsl +6 -9
- package/src/gpu/kernels/grouped_pointwise_conv2d_f16.wgsl +6 -9
- package/src/gpu/kernels/matmul.js +25 -0
- package/src/gpu/kernels/pixel_shuffle.js +1 -1
- package/src/gpu/kernels/pixel_shuffle.wgsl +4 -5
- package/src/gpu/kernels/pixel_shuffle_f16.wgsl +4 -5
- package/src/gpu/kernels/relu.js +15 -2
- package/src/gpu/kernels/relu.wgsl +2 -1
- package/src/gpu/kernels/relu_f16.wgsl +2 -1
- package/src/gpu/kernels/repeat_channels.js +1 -1
- package/src/gpu/kernels/repeat_channels.wgsl +4 -5
- package/src/gpu/kernels/repeat_channels_f16.wgsl +4 -5
- package/src/gpu/kernels/residual.js +44 -8
- package/src/gpu/kernels/residual.wgsl +6 -3
- package/src/gpu/kernels/residual_f16.wgsl +2 -1
- package/src/gpu/kernels/residual_f16_vec4.wgsl +2 -1
- package/src/gpu/kernels/residual_vec4.wgsl +2 -1
- package/src/gpu/kernels/rmsnorm.js +58 -6
- package/src/gpu/kernels/rmsnorm.wgsl +14 -6
- package/src/gpu/kernels/rmsnorm_f16.wgsl +10 -2
- package/src/gpu/kernels/rope.d.ts +2 -0
- package/src/gpu/kernels/rope.js +11 -1
- package/src/gpu/kernels/rope.wgsl +56 -40
- package/src/gpu/kernels/sana_linear_attention.js +1 -2
- package/src/gpu/kernels/sana_linear_attention_apply.wgsl +4 -5
- package/src/gpu/kernels/sana_linear_attention_apply_f16.wgsl +4 -5
- package/src/gpu/kernels/sana_linear_attention_summary.wgsl +4 -0
- package/src/gpu/kernels/sana_linear_attention_summary_f16.wgsl +4 -0
- package/src/gpu/kernels/silu.d.ts +1 -0
- package/src/gpu/kernels/silu.js +32 -14
- package/src/gpu/kernels/silu.wgsl +19 -9
- package/src/gpu/kernels/silu_f16.wgsl +19 -9
- package/src/gpu/kernels/transpose.js +15 -2
- package/src/gpu/kernels/transpose.wgsl +5 -6
- package/src/gpu/kernels/upsample2d.js +2 -1
- package/src/gpu/kernels/upsample2d.wgsl +6 -9
- package/src/gpu/kernels/upsample2d_f16.wgsl +6 -9
- package/src/gpu/kernels/utils.js +16 -1
- package/src/inference/browser-harness.js +47 -1
- package/src/inference/pipelines/diffusion/pipeline.js +15 -6
- package/src/inference/pipelines/diffusion/text-encoder-gpu.d.ts +5 -0
- package/src/inference/pipelines/diffusion/text-encoder-gpu.js +27 -15
- package/src/inference/pipelines/text/attention/record.js +11 -2
- package/src/inference/pipelines/text/attention/run.js +11 -2
- package/src/inference/pipelines/text/chat-format.js +25 -1
- package/src/inference/pipelines/text/config.d.ts +4 -0
- package/src/inference/pipelines/text/config.js +68 -1
- package/src/inference/pipelines/text/execution-plan.js +23 -31
- package/src/inference/pipelines/text/execution-v0.js +29 -2
- package/src/inference/pipelines/text/ffn/standard.js +3 -0
- package/src/inference/pipelines/text/init.d.ts +4 -0
- package/src/inference/pipelines/text/init.js +56 -9
- package/src/inference/pipelines/text/layer.js +11 -0
- package/src/inference/pipelines/text.js +4 -0
- package/src/inference/tokenizers/bundled.js +156 -33
- package/src/rules/tooling/command-runtime.rules.json +18 -0
- package/src/tooling/command-api.d.ts +27 -1
- package/src/tooling/command-api.js +142 -3
- package/src/tooling/node-browser-command-runner.d.ts +4 -0
- package/src/tooling/node-browser-command-runner.js +58 -3
- package/src/tooling/node-command-runner.js +15 -0
- package/src/tooling/node-webgpu.js +9 -87
- package/src/training/checkpoint-watch.d.ts +7 -0
- package/src/training/checkpoint-watch.js +106 -0
- package/src/training/checkpoint.d.ts +6 -1
- package/src/training/checkpoint.js +12 -2
- package/src/training/distillation/artifacts.d.ts +71 -0
- package/src/training/distillation/artifacts.js +132 -0
- package/src/training/distillation/checkpoint-watch.d.ts +10 -0
- package/src/training/distillation/checkpoint-watch.js +57 -0
- package/src/training/distillation/dataset.d.ts +59 -0
- package/src/training/distillation/dataset.js +337 -0
- package/src/training/distillation/eval.d.ts +34 -0
- package/src/training/distillation/eval.js +310 -0
- package/src/training/distillation/index.d.ts +29 -0
- package/src/training/distillation/index.js +29 -0
- package/src/training/distillation/runtime.d.ts +20 -0
- package/src/training/distillation/runtime.js +121 -0
- package/src/training/distillation/scoreboard.d.ts +6 -0
- package/src/training/distillation/scoreboard.js +8 -0
- package/src/training/distillation/stage-a.d.ts +45 -0
- package/src/training/distillation/stage-a.js +338 -0
- package/src/training/distillation/stage-b.d.ts +24 -0
- package/src/training/distillation/stage-b.js +20 -0
- package/src/training/index.d.ts +10 -0
- package/src/training/index.js +10 -0
- package/src/training/lora-pipeline.d.ts +40 -0
- package/src/training/lora-pipeline.js +796 -0
- package/src/training/operator-artifacts.d.ts +62 -0
- package/src/training/operator-artifacts.js +140 -0
- package/src/training/operator-command.d.ts +5 -0
- package/src/training/operator-command.js +453 -0
- package/src/training/operator-eval.d.ts +48 -0
- package/src/training/operator-eval.js +230 -0
- package/src/training/operator-scoreboard.d.ts +5 -0
- package/src/training/operator-scoreboard.js +44 -0
- package/src/training/runner.d.ts +52 -0
- package/src/training/runner.js +29 -4
- package/src/training/suite.d.ts +112 -0
- package/src/training/suite.js +9 -9
- package/src/training/workloads.d.ts +164 -0
- package/src/training/workloads.js +539 -0
- package/src/version.js +1 -1
- package/tools/doppler-cli.js +137 -40
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { LoadedTrainingWorkload, TrainingWorkloadPack } from './workloads.js';
|
|
2
|
+
|
|
3
|
+
export interface TrainingRunLayout {
|
|
4
|
+
runRoot: string;
|
|
5
|
+
logs: string;
|
|
6
|
+
checkpoints: string;
|
|
7
|
+
eval: string;
|
|
8
|
+
scoreboard: string;
|
|
9
|
+
exports: string;
|
|
10
|
+
compare: string;
|
|
11
|
+
qualityGate: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export declare function normalizeTrainingTimestamp(value?: string | Date | null): string;
|
|
15
|
+
|
|
16
|
+
export declare function createTrainingRunLayout(options: {
|
|
17
|
+
kind: string;
|
|
18
|
+
workloadId: string;
|
|
19
|
+
timestamp?: string | Date | null;
|
|
20
|
+
}): Promise<TrainingRunLayout>;
|
|
21
|
+
|
|
22
|
+
export declare function writeJsonArtifact(
|
|
23
|
+
filePath: string,
|
|
24
|
+
payload: Record<string, unknown>
|
|
25
|
+
): Promise<{ path: string; sha256: string; relativePath: string }>;
|
|
26
|
+
|
|
27
|
+
export declare function writeNdjsonRow(
|
|
28
|
+
filePath: string,
|
|
29
|
+
row: Record<string, unknown>
|
|
30
|
+
): Promise<{ path: string; sha256: string; relativePath: string }>;
|
|
31
|
+
|
|
32
|
+
export declare function writeWorkloadLock(
|
|
33
|
+
layout: TrainingRunLayout,
|
|
34
|
+
loadedWorkload: LoadedTrainingWorkload
|
|
35
|
+
): Promise<{ path: string; sha256: string; relativePath: string }>;
|
|
36
|
+
|
|
37
|
+
export declare function writeRunContract(
|
|
38
|
+
layout: TrainingRunLayout,
|
|
39
|
+
payload: Record<string, unknown>
|
|
40
|
+
): Promise<{ path: string; sha256: string; relativePath: string }>;
|
|
41
|
+
|
|
42
|
+
export declare function buildArtifactBase(options: {
|
|
43
|
+
artifactType: string;
|
|
44
|
+
reportId: string;
|
|
45
|
+
workload: TrainingWorkloadPack;
|
|
46
|
+
workloadPath: string;
|
|
47
|
+
workloadSha256: string;
|
|
48
|
+
datasetPath: string | null;
|
|
49
|
+
datasetHash: string | null;
|
|
50
|
+
baseModelId: string | null;
|
|
51
|
+
teacherModelId?: string | null;
|
|
52
|
+
studentModelId?: string | null;
|
|
53
|
+
stage?: string | null;
|
|
54
|
+
checkpointStep?: number | null;
|
|
55
|
+
parentArtifacts?: Array<Record<string, unknown>>;
|
|
56
|
+
runtime?: string;
|
|
57
|
+
surface?: string;
|
|
58
|
+
claimBoundary: string;
|
|
59
|
+
configHash: string;
|
|
60
|
+
}): Record<string, unknown>;
|
|
61
|
+
|
|
62
|
+
export declare function hashArtifactPayload(payload: Record<string, unknown>): string;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { sha256Hex } from '../utils/sha256.js';
|
|
5
|
+
import { serializeTrainingWorkloadLock } from './workloads.js';
|
|
6
|
+
|
|
7
|
+
function stableSortObject(value) {
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
return value.map((entry) => stableSortObject(entry));
|
|
10
|
+
}
|
|
11
|
+
if (!value || typeof value !== 'object') {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
const sorted = {};
|
|
15
|
+
for (const key of Object.keys(value).sort()) {
|
|
16
|
+
sorted[key] = stableSortObject(value[key]);
|
|
17
|
+
}
|
|
18
|
+
return sorted;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function stableJson(value) {
|
|
22
|
+
return JSON.stringify(stableSortObject(value));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function normalizeTrainingTimestamp(value = null) {
|
|
26
|
+
const date = value instanceof Date
|
|
27
|
+
? value
|
|
28
|
+
: (typeof value === 'string' && value.trim() ? new Date(value) : new Date());
|
|
29
|
+
return date.toISOString().replace(/[:]/g, '-');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function createTrainingRunLayout({ kind, workloadId, timestamp = null }) {
|
|
33
|
+
const normalizedKind = String(kind || '').trim();
|
|
34
|
+
const normalizedWorkloadId = String(workloadId || '').trim();
|
|
35
|
+
if (!normalizedKind || !normalizedWorkloadId) {
|
|
36
|
+
throw new Error('createTrainingRunLayout requires kind and workloadId.');
|
|
37
|
+
}
|
|
38
|
+
const ts = normalizeTrainingTimestamp(timestamp);
|
|
39
|
+
const runRoot = resolve('reports', 'training', normalizedKind, normalizedWorkloadId, ts);
|
|
40
|
+
const directories = {
|
|
41
|
+
runRoot,
|
|
42
|
+
logs: join(runRoot, 'logs'),
|
|
43
|
+
checkpoints: join(runRoot, 'checkpoints'),
|
|
44
|
+
eval: join(runRoot, 'eval'),
|
|
45
|
+
scoreboard: join(runRoot, 'scoreboard'),
|
|
46
|
+
exports: join(runRoot, 'exports'),
|
|
47
|
+
compare: join(runRoot, 'compare'),
|
|
48
|
+
qualityGate: join(runRoot, 'quality-gate'),
|
|
49
|
+
};
|
|
50
|
+
await Promise.all(Object.values(directories).map((dirPath) => mkdir(dirPath, { recursive: true })));
|
|
51
|
+
return directories;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function writeJsonArtifact(filePath, payload) {
|
|
55
|
+
const absolutePath = resolve(String(filePath));
|
|
56
|
+
const json = `${JSON.stringify(payload, null, 2)}\n`;
|
|
57
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
58
|
+
await writeFile(absolutePath, json, 'utf8');
|
|
59
|
+
return {
|
|
60
|
+
path: absolutePath,
|
|
61
|
+
sha256: sha256Hex(json),
|
|
62
|
+
relativePath: relative(process.cwd(), absolutePath),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function writeNdjsonRow(filePath, row) {
|
|
67
|
+
const absolutePath = resolve(String(filePath));
|
|
68
|
+
let existing = '';
|
|
69
|
+
try {
|
|
70
|
+
existing = await readFile(absolutePath, 'utf8');
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error?.code !== 'ENOENT') {
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const serialized = `${existing}${JSON.stringify(row)}\n`;
|
|
77
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
78
|
+
await writeFile(absolutePath, serialized, 'utf8');
|
|
79
|
+
return {
|
|
80
|
+
path: absolutePath,
|
|
81
|
+
sha256: sha256Hex(serialized),
|
|
82
|
+
relativePath: relative(process.cwd(), absolutePath),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function writeWorkloadLock(layout, loadedWorkload) {
|
|
87
|
+
const filePath = join(layout.runRoot, 'workload.lock.json');
|
|
88
|
+
const payload = JSON.parse(serializeTrainingWorkloadLock(loadedWorkload));
|
|
89
|
+
return writeJsonArtifact(filePath, payload);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function writeRunContract(layout, payload) {
|
|
93
|
+
return writeJsonArtifact(join(layout.runRoot, 'run_contract.json'), payload);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function buildArtifactBase({
|
|
97
|
+
artifactType,
|
|
98
|
+
reportId,
|
|
99
|
+
workload,
|
|
100
|
+
workloadPath,
|
|
101
|
+
workloadSha256,
|
|
102
|
+
datasetPath,
|
|
103
|
+
datasetHash,
|
|
104
|
+
baseModelId,
|
|
105
|
+
teacherModelId = null,
|
|
106
|
+
studentModelId = null,
|
|
107
|
+
stage = null,
|
|
108
|
+
checkpointStep = null,
|
|
109
|
+
parentArtifacts = [],
|
|
110
|
+
runtime = 'node',
|
|
111
|
+
surface = 'node',
|
|
112
|
+
claimBoundary,
|
|
113
|
+
configHash,
|
|
114
|
+
}) {
|
|
115
|
+
return {
|
|
116
|
+
artifactType,
|
|
117
|
+
schemaVersion: 1,
|
|
118
|
+
reportId,
|
|
119
|
+
workloadId: workload.id,
|
|
120
|
+
workloadPath,
|
|
121
|
+
workloadSha256,
|
|
122
|
+
configHash,
|
|
123
|
+
datasetPath: datasetPath || null,
|
|
124
|
+
datasetHash: datasetHash || null,
|
|
125
|
+
baseModelId: baseModelId || null,
|
|
126
|
+
teacherModelId: teacherModelId || null,
|
|
127
|
+
studentModelId: studentModelId || null,
|
|
128
|
+
stage,
|
|
129
|
+
checkpointStep,
|
|
130
|
+
parentArtifacts,
|
|
131
|
+
generatedAt: new Date().toISOString(),
|
|
132
|
+
runtime,
|
|
133
|
+
surface,
|
|
134
|
+
claimBoundary,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function hashArtifactPayload(payload) {
|
|
139
|
+
return sha256Hex(stableJson(payload));
|
|
140
|
+
}
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { loadTrainingWorkloadPack } from './workloads.js';
|
|
5
|
+
import {
|
|
6
|
+
buildFrozenSubset,
|
|
7
|
+
createDistillationRunArtifacts,
|
|
8
|
+
evaluateDistillationCheckpoint,
|
|
9
|
+
runDistillationStageA,
|
|
10
|
+
runDistillationStageB,
|
|
11
|
+
watchDistillationCheckpoints,
|
|
12
|
+
} from './distillation/index.js';
|
|
13
|
+
import {
|
|
14
|
+
compareLoraRun,
|
|
15
|
+
evaluateLoraCheckpoint,
|
|
16
|
+
exportLoraCheckpoint,
|
|
17
|
+
qualityGateLoraRun,
|
|
18
|
+
runLoraPipeline,
|
|
19
|
+
watchLoraCheckpoints,
|
|
20
|
+
} from './lora-pipeline.js';
|
|
21
|
+
import { writeDistillCompareReport, writeDistillQualityGateReport } from './distillation/artifacts.js';
|
|
22
|
+
|
|
23
|
+
async function listJsonFiles(rootDir) {
|
|
24
|
+
const results = [];
|
|
25
|
+
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
const absolutePath = join(rootDir, entry.name);
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
results.push(...await listJsonFiles(absolutePath));
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
33
|
+
results.push(absolutePath);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return results.sort((left, right) => left.localeCompare(right));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function readJson(filePath) {
|
|
40
|
+
const raw = await readFile(filePath, 'utf8');
|
|
41
|
+
return JSON.parse(raw);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function loadWorkloadFromRunRoot(runRoot) {
|
|
45
|
+
const lockPath = join(resolve(String(runRoot)), 'workload.lock.json');
|
|
46
|
+
const payload = await readJson(lockPath);
|
|
47
|
+
return {
|
|
48
|
+
absolutePath: payload.workloadPath,
|
|
49
|
+
path: payload.workloadPath,
|
|
50
|
+
raw: JSON.stringify(payload.workload),
|
|
51
|
+
workloadSha256: payload.workloadSha256,
|
|
52
|
+
workload: payload.workload,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveStageEntry(workload, action) {
|
|
57
|
+
const stagePlan = Array.isArray(workload.pipeline?.stagePlan) ? workload.pipeline.stagePlan : [];
|
|
58
|
+
if (action === 'stage-a') {
|
|
59
|
+
return stagePlan.find((entry) => String(entry.trainingStage || entry.id || '').includes('stage_a')) || stagePlan[0];
|
|
60
|
+
}
|
|
61
|
+
if (action === 'stage-b') {
|
|
62
|
+
return stagePlan.find((entry) => String(entry.trainingStage || entry.id || '').includes('stage_b'))
|
|
63
|
+
|| stagePlan[stagePlan.length - 1];
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function buildDistillSubsetIfNeeded(loadedWorkload, layout, subsetManifestPath = null) {
|
|
69
|
+
if (subsetManifestPath) {
|
|
70
|
+
const manifest = await readJson(subsetManifestPath);
|
|
71
|
+
return {
|
|
72
|
+
subsetManifestPath: resolve(String(subsetManifestPath)),
|
|
73
|
+
subsetJsonlPath: manifest.output?.subsetJsonlPath || loadedWorkload.workload.datasetPath,
|
|
74
|
+
manifest,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (!loadedWorkload.workload.pipeline.subsetSpec) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const subset = await buildFrozenSubset({
|
|
81
|
+
datasetPath: loadedWorkload.workload.datasetPath,
|
|
82
|
+
outputDir: join(layout.exports, 'subset'),
|
|
83
|
+
strictPairContract: loadedWorkload.workload.pipeline.strictPairContract === true,
|
|
84
|
+
sourceLangs: loadedWorkload.workload.pipeline.sourceLangs,
|
|
85
|
+
targetLangs: loadedWorkload.workload.pipeline.targetLangs,
|
|
86
|
+
pairAllowlist: loadedWorkload.workload.pipeline.pairAllowlist,
|
|
87
|
+
subsetSpec: loadedWorkload.workload.pipeline.subsetSpec,
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
subsetManifestPath: subset.manifestPath,
|
|
91
|
+
subsetJsonlPath: subset.subsetJsonlPath,
|
|
92
|
+
manifest: subset.manifest,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function compareDistillRun(runRoot) {
|
|
97
|
+
const evalDir = join(runRoot, 'eval');
|
|
98
|
+
const files = await listJsonFiles(evalDir);
|
|
99
|
+
const reports = [];
|
|
100
|
+
for (const filePath of files) {
|
|
101
|
+
const report = await readJson(filePath);
|
|
102
|
+
if (report?.artifactType === 'training_eval_report') {
|
|
103
|
+
reports.push(report);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const sorted = reports
|
|
107
|
+
.slice()
|
|
108
|
+
.sort((left, right) => Number(right?.primaryScore ?? Number.NEGATIVE_INFINITY) - Number(left?.primaryScore ?? Number.NEGATIVE_INFINITY));
|
|
109
|
+
const payload = {
|
|
110
|
+
artifactType: 'training_compare_report',
|
|
111
|
+
schemaVersion: 1,
|
|
112
|
+
generatedAt: new Date().toISOString(),
|
|
113
|
+
runRoot,
|
|
114
|
+
count: sorted.length,
|
|
115
|
+
best: sorted[0] || null,
|
|
116
|
+
reports: sorted.map((report) => ({
|
|
117
|
+
stage: report.stage || null,
|
|
118
|
+
checkpointId: report.checkpointId || null,
|
|
119
|
+
evalDatasetId: report.evalDatasetId || null,
|
|
120
|
+
primaryMetric: report.primaryMetric || null,
|
|
121
|
+
primaryScore: report.primaryScore ?? null,
|
|
122
|
+
bleu: report.bleu ?? null,
|
|
123
|
+
chrf: report.chrf ?? null,
|
|
124
|
+
reportPath: report.reportPath || null,
|
|
125
|
+
})),
|
|
126
|
+
};
|
|
127
|
+
const artifact = await writeDistillCompareReport({
|
|
128
|
+
compare: join(runRoot, 'compare'),
|
|
129
|
+
}, payload);
|
|
130
|
+
return {
|
|
131
|
+
...payload,
|
|
132
|
+
comparePath: artifact.path,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function qualityGateDistillRun(runRoot, loadedWorkload) {
|
|
137
|
+
const checks = [];
|
|
138
|
+
const required = [
|
|
139
|
+
join(runRoot, 'run_contract.json'),
|
|
140
|
+
join(runRoot, 'workload.lock.json'),
|
|
141
|
+
];
|
|
142
|
+
for (const filePath of required) {
|
|
143
|
+
try {
|
|
144
|
+
await readFile(filePath, 'utf8');
|
|
145
|
+
checks.push({ path: filePath, ok: true });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
checks.push({ path: filePath, ok: false, error: error?.message || String(error) });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const stageManifests = await listJsonFiles(join(runRoot, 'checkpoints'));
|
|
151
|
+
const expectedStageCount = Array.isArray(loadedWorkload.workload.pipeline?.stagePlan)
|
|
152
|
+
? loadedWorkload.workload.pipeline.stagePlan.length
|
|
153
|
+
: 0;
|
|
154
|
+
const actualStageCount = stageManifests.filter((filePath) => filePath.endsWith('distill_stage_manifest.json')).length;
|
|
155
|
+
checks.push({
|
|
156
|
+
path: join(runRoot, 'checkpoints'),
|
|
157
|
+
ok: actualStageCount >= expectedStageCount,
|
|
158
|
+
expectedStageCount,
|
|
159
|
+
actualStageCount,
|
|
160
|
+
});
|
|
161
|
+
const passed = checks.every((entry) => entry.ok === true);
|
|
162
|
+
const payload = {
|
|
163
|
+
artifactType: 'training_quality_gate',
|
|
164
|
+
schemaVersion: 1,
|
|
165
|
+
generatedAt: new Date().toISOString(),
|
|
166
|
+
runRoot,
|
|
167
|
+
passed,
|
|
168
|
+
checks,
|
|
169
|
+
};
|
|
170
|
+
const artifact = await writeDistillQualityGateReport({
|
|
171
|
+
qualityGate: join(runRoot, 'quality-gate'),
|
|
172
|
+
}, payload);
|
|
173
|
+
return {
|
|
174
|
+
...payload,
|
|
175
|
+
reportPath: artifact.path,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function runDistillCommand(request) {
|
|
180
|
+
const action = String(request.action || '').trim();
|
|
181
|
+
const loadedWorkload = request.workloadPath
|
|
182
|
+
? await loadTrainingWorkloadPack(request.workloadPath)
|
|
183
|
+
: await loadWorkloadFromRunRoot(request.runRoot);
|
|
184
|
+
const runArtifacts = await createDistillationRunArtifacts({
|
|
185
|
+
loadedWorkload,
|
|
186
|
+
runRoot: request.runRoot || null,
|
|
187
|
+
timestamp: request.timestamp || null,
|
|
188
|
+
});
|
|
189
|
+
if (action === 'subsets') {
|
|
190
|
+
const subset = await buildDistillSubsetIfNeeded(loadedWorkload, runArtifacts.layout, request.subsetManifest || null);
|
|
191
|
+
if (!subset) {
|
|
192
|
+
throw new Error(`Distill workload "${loadedWorkload.workload.id}" has no subsetSpec and no subsetManifest was provided.`);
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
ok: true,
|
|
196
|
+
kind: 'distill',
|
|
197
|
+
action,
|
|
198
|
+
workloadId: loadedWorkload.workload.id,
|
|
199
|
+
runRoot: runArtifacts.layout.runRoot,
|
|
200
|
+
subset,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (action === 'run' || action === 'stage-a' || action === 'stage-b') {
|
|
204
|
+
const subset = await buildDistillSubsetIfNeeded(loadedWorkload, runArtifacts.layout, request.subsetManifest || null);
|
|
205
|
+
const datasetPath = subset?.subsetJsonlPath || loadedWorkload.workload.datasetPath;
|
|
206
|
+
const stageResults = [];
|
|
207
|
+
if (action === 'run') {
|
|
208
|
+
for (const stageEntry of loadedWorkload.workload.pipeline.stagePlan) {
|
|
209
|
+
const stageResult = String(stageEntry.trainingStage || stageEntry.id || '').includes('stage_b')
|
|
210
|
+
? await runDistillationStageB({
|
|
211
|
+
loadedWorkload,
|
|
212
|
+
stageEntry,
|
|
213
|
+
layout: runArtifacts.layout,
|
|
214
|
+
datasetPath,
|
|
215
|
+
priorStageResult: stageResults[stageResults.length - 1] || null,
|
|
216
|
+
legacyArtifactDir: join(runArtifacts.layout.runRoot, 'legacy-stage-artifacts'),
|
|
217
|
+
timestamp: request.timestamp || null,
|
|
218
|
+
})
|
|
219
|
+
: await runDistillationStageA({
|
|
220
|
+
loadedWorkload,
|
|
221
|
+
stageEntry,
|
|
222
|
+
layout: runArtifacts.layout,
|
|
223
|
+
datasetPath,
|
|
224
|
+
legacyArtifactDir: join(runArtifacts.layout.runRoot, 'legacy-stage-artifacts'),
|
|
225
|
+
timestamp: request.timestamp || null,
|
|
226
|
+
});
|
|
227
|
+
stageResults.push(stageResult);
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
const stageEntry = resolveStageEntry(loadedWorkload.workload, action);
|
|
231
|
+
if (!stageEntry) {
|
|
232
|
+
throw new Error(`Unable to resolve stage entry for action "${action}".`);
|
|
233
|
+
}
|
|
234
|
+
const stageResult = action === 'stage-b'
|
|
235
|
+
? await runDistillationStageB({
|
|
236
|
+
loadedWorkload,
|
|
237
|
+
stageEntry,
|
|
238
|
+
layout: runArtifacts.layout,
|
|
239
|
+
datasetPath,
|
|
240
|
+
stageAArtifact: request.stageArtifact || null,
|
|
241
|
+
legacyArtifactDir: join(runArtifacts.layout.runRoot, 'legacy-stage-artifacts'),
|
|
242
|
+
timestamp: request.timestamp || null,
|
|
243
|
+
})
|
|
244
|
+
: await runDistillationStageA({
|
|
245
|
+
loadedWorkload,
|
|
246
|
+
stageEntry,
|
|
247
|
+
layout: runArtifacts.layout,
|
|
248
|
+
datasetPath,
|
|
249
|
+
legacyArtifactDir: join(runArtifacts.layout.runRoot, 'legacy-stage-artifacts'),
|
|
250
|
+
timestamp: request.timestamp || null,
|
|
251
|
+
});
|
|
252
|
+
stageResults.push(stageResult);
|
|
253
|
+
}
|
|
254
|
+
const compare = await compareDistillRun(runArtifacts.layout.runRoot);
|
|
255
|
+
const qualityGate = await qualityGateDistillRun(runArtifacts.layout.runRoot, loadedWorkload);
|
|
256
|
+
return {
|
|
257
|
+
ok: true,
|
|
258
|
+
kind: 'distill',
|
|
259
|
+
action,
|
|
260
|
+
workloadId: loadedWorkload.workload.id,
|
|
261
|
+
runRoot: runArtifacts.layout.runRoot,
|
|
262
|
+
subsetManifest: subset?.subsetManifestPath || null,
|
|
263
|
+
stageResults,
|
|
264
|
+
compare,
|
|
265
|
+
qualityGate,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (action === 'eval') {
|
|
269
|
+
if (request.checkpointPath) {
|
|
270
|
+
return {
|
|
271
|
+
ok: true,
|
|
272
|
+
kind: 'distill',
|
|
273
|
+
action,
|
|
274
|
+
reports: await evaluateDistillationCheckpoint({
|
|
275
|
+
loadedWorkload,
|
|
276
|
+
checkpointPath: request.checkpointPath,
|
|
277
|
+
checkpointId: request.checkpointId || null,
|
|
278
|
+
checkpointStep: request.checkpointStep ?? null,
|
|
279
|
+
stageId: request.stageId || null,
|
|
280
|
+
evalDatasetId: request.evalDatasetId || null,
|
|
281
|
+
layout: runArtifacts.layout,
|
|
282
|
+
}),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const markerFiles = (await listJsonFiles(join(runArtifacts.layout.runRoot, 'checkpoints')))
|
|
286
|
+
.filter((filePath) => filePath.endsWith('checkpoint.complete.json'));
|
|
287
|
+
const reports = [];
|
|
288
|
+
for (const markerPath of markerFiles) {
|
|
289
|
+
const marker = await readJson(markerPath);
|
|
290
|
+
reports.push(...await evaluateDistillationCheckpoint({
|
|
291
|
+
loadedWorkload,
|
|
292
|
+
checkpointPath: marker.checkpointPath,
|
|
293
|
+
checkpointId: marker.checkpointId || null,
|
|
294
|
+
checkpointStep: marker.checkpointStep ?? null,
|
|
295
|
+
stageId: marker.stage || null,
|
|
296
|
+
evalDatasetId: request.evalDatasetId || null,
|
|
297
|
+
layout: runArtifacts.layout,
|
|
298
|
+
stageAArtifact: marker.stageArtifact || null,
|
|
299
|
+
stageAArtifactHash: marker.stageArtifactHash || null,
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
ok: true,
|
|
304
|
+
kind: 'distill',
|
|
305
|
+
action,
|
|
306
|
+
reports,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (action === 'watch') {
|
|
310
|
+
return {
|
|
311
|
+
ok: true,
|
|
312
|
+
kind: 'distill',
|
|
313
|
+
action,
|
|
314
|
+
...(await watchDistillationCheckpoints({
|
|
315
|
+
loadedWorkload,
|
|
316
|
+
layout: runArtifacts.layout,
|
|
317
|
+
pollIntervalMs: request.pollIntervalMs || null,
|
|
318
|
+
stopWhenIdle: request.stopWhenIdle === true,
|
|
319
|
+
})),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (action === 'compare') {
|
|
323
|
+
return {
|
|
324
|
+
ok: true,
|
|
325
|
+
kind: 'distill',
|
|
326
|
+
action,
|
|
327
|
+
...(await compareDistillRun(runArtifacts.layout.runRoot)),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (action === 'quality-gate') {
|
|
331
|
+
return {
|
|
332
|
+
ok: true,
|
|
333
|
+
kind: 'distill',
|
|
334
|
+
action,
|
|
335
|
+
...(await qualityGateDistillRun(runArtifacts.layout.runRoot, loadedWorkload)),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
throw new Error(`Unsupported distill action "${action}".`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function runLoraCommand(request) {
|
|
342
|
+
const action = String(request.action || '').trim();
|
|
343
|
+
const loadedWorkload = request.workloadPath
|
|
344
|
+
? await loadTrainingWorkloadPack(request.workloadPath)
|
|
345
|
+
: await loadWorkloadFromRunRoot(request.runRoot);
|
|
346
|
+
if (action === 'run') {
|
|
347
|
+
return runLoraPipeline({
|
|
348
|
+
loadedWorkload,
|
|
349
|
+
runRoot: request.runRoot || null,
|
|
350
|
+
timestamp: request.timestamp || null,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
if (action === 'eval') {
|
|
354
|
+
const checkpointPath = request.checkpointPath
|
|
355
|
+
|| (await selectLoraCheckpointPath(request.runRoot)).checkpointPath;
|
|
356
|
+
return {
|
|
357
|
+
ok: true,
|
|
358
|
+
kind: 'lora',
|
|
359
|
+
action,
|
|
360
|
+
reports: await evaluateLoraCheckpoint({
|
|
361
|
+
loadedWorkload,
|
|
362
|
+
checkpointPath,
|
|
363
|
+
checkpointId: request.checkpointId || null,
|
|
364
|
+
checkpointStep: request.checkpointStep ?? null,
|
|
365
|
+
layout: request.runRoot
|
|
366
|
+
? { eval: join(resolve(String(request.runRoot)), 'eval') }
|
|
367
|
+
: null,
|
|
368
|
+
}),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
if (action === 'watch') {
|
|
372
|
+
return {
|
|
373
|
+
ok: true,
|
|
374
|
+
kind: 'lora',
|
|
375
|
+
action,
|
|
376
|
+
...(await watchLoraCheckpoints({
|
|
377
|
+
loadedWorkload,
|
|
378
|
+
runRoot: resolve(String(request.runRoot)),
|
|
379
|
+
pollIntervalMs: request.pollIntervalMs || null,
|
|
380
|
+
stopWhenIdle: request.stopWhenIdle === true,
|
|
381
|
+
})),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (action === 'export') {
|
|
385
|
+
const checkpointSelection = request.checkpointPath
|
|
386
|
+
? { checkpointPath: request.checkpointPath, checkpointId: request.checkpointId || null }
|
|
387
|
+
: await selectLoraCheckpointPath(request.runRoot);
|
|
388
|
+
return {
|
|
389
|
+
ok: true,
|
|
390
|
+
kind: 'lora',
|
|
391
|
+
action,
|
|
392
|
+
...(await exportLoraCheckpoint({
|
|
393
|
+
loadedWorkload,
|
|
394
|
+
checkpointPath: checkpointSelection.checkpointPath,
|
|
395
|
+
checkpointId: checkpointSelection.checkpointId || null,
|
|
396
|
+
layout: request.runRoot
|
|
397
|
+
? { exports: join(resolve(String(request.runRoot)), 'exports') }
|
|
398
|
+
: null,
|
|
399
|
+
})),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (action === 'compare') {
|
|
403
|
+
return {
|
|
404
|
+
ok: true,
|
|
405
|
+
kind: 'lora',
|
|
406
|
+
action,
|
|
407
|
+
...(await compareLoraRun({
|
|
408
|
+
runRoot: resolve(String(request.runRoot)),
|
|
409
|
+
})),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
if (action === 'quality-gate') {
|
|
413
|
+
return {
|
|
414
|
+
ok: true,
|
|
415
|
+
kind: 'lora',
|
|
416
|
+
action,
|
|
417
|
+
...(await qualityGateLoraRun({
|
|
418
|
+
runRoot: resolve(String(request.runRoot)),
|
|
419
|
+
})),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
if (action === 'activate') {
|
|
423
|
+
throw new Error(
|
|
424
|
+
'lora activate is not supported in the Node operator runner. The active-model adapter surface currently lives in the browser provider.'
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
throw new Error(`Unsupported lora action "${action}".`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function selectLoraCheckpointPath(runRoot) {
|
|
431
|
+
const checkpointsDir = join(resolve(String(runRoot)), 'checkpoints');
|
|
432
|
+
const markers = (await listJsonFiles(checkpointsDir))
|
|
433
|
+
.filter((filePath) => filePath.endsWith('checkpoint.complete.json'));
|
|
434
|
+
const latest = markers[markers.length - 1];
|
|
435
|
+
if (!latest) {
|
|
436
|
+
throw new Error(`No finalized LoRA checkpoints found in ${checkpointsDir}.`);
|
|
437
|
+
}
|
|
438
|
+
const marker = await readJson(latest);
|
|
439
|
+
return {
|
|
440
|
+
checkpointPath: marker.checkpointPath,
|
|
441
|
+
checkpointId: marker.checkpointId || null,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export async function runTrainingOperatorCommand(request) {
|
|
446
|
+
if (request.command === 'distill') {
|
|
447
|
+
return runDistillCommand(request);
|
|
448
|
+
}
|
|
449
|
+
if (request.command === 'lora') {
|
|
450
|
+
return runLoraCommand(request);
|
|
451
|
+
}
|
|
452
|
+
throw new Error(`Unsupported training operator command "${request.command}".`);
|
|
453
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface BleuResult {
|
|
2
|
+
score: number;
|
|
3
|
+
brevityPenalty: number;
|
|
4
|
+
precisions: number[];
|
|
5
|
+
hypothesisLength: number;
|
|
6
|
+
referenceLength: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ChrfResult {
|
|
10
|
+
score: number;
|
|
11
|
+
precision: number;
|
|
12
|
+
recall: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export declare function computeBleuScore(
|
|
16
|
+
hypotheses: string[],
|
|
17
|
+
references: string[],
|
|
18
|
+
options?: { maxOrder?: number }
|
|
19
|
+
): BleuResult;
|
|
20
|
+
|
|
21
|
+
export declare function computeChrfScore(
|
|
22
|
+
hypotheses: string[],
|
|
23
|
+
references: string[],
|
|
24
|
+
options?: { maxOrder?: number; beta?: number }
|
|
25
|
+
): ChrfResult;
|
|
26
|
+
|
|
27
|
+
export declare function computeExactMatch(
|
|
28
|
+
hypotheses: string[],
|
|
29
|
+
references: string[]
|
|
30
|
+
): { score: number; matches: number; total: number };
|
|
31
|
+
|
|
32
|
+
export declare function computeAccuracy(
|
|
33
|
+
labels: string[],
|
|
34
|
+
predictions: string[]
|
|
35
|
+
): { score: number; matches: number; total: number };
|
|
36
|
+
|
|
37
|
+
export declare function computeEvalMetrics(
|
|
38
|
+
evalKind: string,
|
|
39
|
+
hypotheses: string[],
|
|
40
|
+
references: string[],
|
|
41
|
+
options?: Record<string, unknown>
|
|
42
|
+
): Record<string, unknown>;
|
|
43
|
+
|
|
44
|
+
export declare function loadEvalDataset(datasetPath: string): Promise<{
|
|
45
|
+
absolutePath: string;
|
|
46
|
+
rows: unknown[];
|
|
47
|
+
raw: string;
|
|
48
|
+
}>;
|