@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.
Files changed (130) hide show
  1. package/README.md +23 -8
  2. package/package.json +7 -4
  3. package/src/config/kernels/kernel-ref-digests.js +39 -39
  4. package/src/config/kernels/registry.json +42 -2
  5. package/src/config/loader.js +31 -2
  6. package/src/config/merge.js +18 -0
  7. package/src/config/presets/models/qwen3.json +9 -2
  8. package/src/config/presets/models/transformer.json +5 -0
  9. package/src/config/required-inference-fields-contract-check.js +6 -0
  10. package/src/config/schema/inference-defaults.schema.js +3 -0
  11. package/src/config/schema/inference.schema.d.ts +9 -0
  12. package/src/config/schema/kernel-path.schema.d.ts +6 -0
  13. package/src/config/schema/manifest.schema.d.ts +6 -0
  14. package/src/config/schema/manifest.schema.js +3 -0
  15. package/src/converter/rope-config.js +42 -0
  16. package/src/gpu/device.js +58 -0
  17. package/src/gpu/kernels/attention.js +98 -0
  18. package/src/gpu/kernels/bias_add.wgsl +8 -6
  19. package/src/gpu/kernels/bias_add_f16.wgsl +8 -5
  20. package/src/gpu/kernels/conv2d.js +1 -1
  21. package/src/gpu/kernels/conv2d.wgsl +7 -8
  22. package/src/gpu/kernels/conv2d_f16.wgsl +7 -8
  23. package/src/gpu/kernels/depthwise_conv2d.js +2 -1
  24. package/src/gpu/kernels/depthwise_conv2d.wgsl +6 -9
  25. package/src/gpu/kernels/depthwise_conv2d_f16.wgsl +6 -9
  26. package/src/gpu/kernels/grouped_pointwise_conv2d.js +2 -1
  27. package/src/gpu/kernels/grouped_pointwise_conv2d.wgsl +6 -9
  28. package/src/gpu/kernels/grouped_pointwise_conv2d_f16.wgsl +6 -9
  29. package/src/gpu/kernels/matmul.js +25 -0
  30. package/src/gpu/kernels/pixel_shuffle.js +1 -1
  31. package/src/gpu/kernels/pixel_shuffle.wgsl +4 -5
  32. package/src/gpu/kernels/pixel_shuffle_f16.wgsl +4 -5
  33. package/src/gpu/kernels/relu.js +15 -2
  34. package/src/gpu/kernels/relu.wgsl +2 -1
  35. package/src/gpu/kernels/relu_f16.wgsl +2 -1
  36. package/src/gpu/kernels/repeat_channels.js +1 -1
  37. package/src/gpu/kernels/repeat_channels.wgsl +4 -5
  38. package/src/gpu/kernels/repeat_channels_f16.wgsl +4 -5
  39. package/src/gpu/kernels/residual.js +44 -8
  40. package/src/gpu/kernels/residual.wgsl +6 -3
  41. package/src/gpu/kernels/residual_f16.wgsl +2 -1
  42. package/src/gpu/kernels/residual_f16_vec4.wgsl +2 -1
  43. package/src/gpu/kernels/residual_vec4.wgsl +2 -1
  44. package/src/gpu/kernels/rmsnorm.js +58 -6
  45. package/src/gpu/kernels/rmsnorm.wgsl +14 -6
  46. package/src/gpu/kernels/rmsnorm_f16.wgsl +10 -2
  47. package/src/gpu/kernels/rope.d.ts +2 -0
  48. package/src/gpu/kernels/rope.js +11 -1
  49. package/src/gpu/kernels/rope.wgsl +56 -40
  50. package/src/gpu/kernels/sana_linear_attention.js +1 -2
  51. package/src/gpu/kernels/sana_linear_attention_apply.wgsl +4 -5
  52. package/src/gpu/kernels/sana_linear_attention_apply_f16.wgsl +4 -5
  53. package/src/gpu/kernels/sana_linear_attention_summary.wgsl +4 -0
  54. package/src/gpu/kernels/sana_linear_attention_summary_f16.wgsl +4 -0
  55. package/src/gpu/kernels/silu.d.ts +1 -0
  56. package/src/gpu/kernels/silu.js +32 -14
  57. package/src/gpu/kernels/silu.wgsl +19 -9
  58. package/src/gpu/kernels/silu_f16.wgsl +19 -9
  59. package/src/gpu/kernels/transpose.js +15 -2
  60. package/src/gpu/kernels/transpose.wgsl +5 -6
  61. package/src/gpu/kernels/upsample2d.js +2 -1
  62. package/src/gpu/kernels/upsample2d.wgsl +6 -9
  63. package/src/gpu/kernels/upsample2d_f16.wgsl +6 -9
  64. package/src/gpu/kernels/utils.js +16 -1
  65. package/src/inference/browser-harness.js +47 -1
  66. package/src/inference/pipelines/diffusion/pipeline.js +15 -6
  67. package/src/inference/pipelines/diffusion/text-encoder-gpu.d.ts +5 -0
  68. package/src/inference/pipelines/diffusion/text-encoder-gpu.js +27 -15
  69. package/src/inference/pipelines/text/attention/record.js +11 -2
  70. package/src/inference/pipelines/text/attention/run.js +11 -2
  71. package/src/inference/pipelines/text/chat-format.js +25 -1
  72. package/src/inference/pipelines/text/config.d.ts +4 -0
  73. package/src/inference/pipelines/text/config.js +68 -1
  74. package/src/inference/pipelines/text/execution-plan.js +23 -31
  75. package/src/inference/pipelines/text/execution-v0.js +29 -2
  76. package/src/inference/pipelines/text/ffn/standard.js +3 -0
  77. package/src/inference/pipelines/text/init.d.ts +4 -0
  78. package/src/inference/pipelines/text/init.js +56 -9
  79. package/src/inference/pipelines/text/layer.js +11 -0
  80. package/src/inference/pipelines/text.js +4 -0
  81. package/src/inference/tokenizers/bundled.js +156 -33
  82. package/src/rules/tooling/command-runtime.rules.json +18 -0
  83. package/src/tooling/command-api.d.ts +27 -1
  84. package/src/tooling/command-api.js +142 -3
  85. package/src/tooling/node-browser-command-runner.d.ts +4 -0
  86. package/src/tooling/node-browser-command-runner.js +58 -3
  87. package/src/tooling/node-command-runner.js +15 -0
  88. package/src/tooling/node-webgpu.js +9 -87
  89. package/src/training/checkpoint-watch.d.ts +7 -0
  90. package/src/training/checkpoint-watch.js +106 -0
  91. package/src/training/checkpoint.d.ts +6 -1
  92. package/src/training/checkpoint.js +12 -2
  93. package/src/training/distillation/artifacts.d.ts +71 -0
  94. package/src/training/distillation/artifacts.js +132 -0
  95. package/src/training/distillation/checkpoint-watch.d.ts +10 -0
  96. package/src/training/distillation/checkpoint-watch.js +57 -0
  97. package/src/training/distillation/dataset.d.ts +59 -0
  98. package/src/training/distillation/dataset.js +337 -0
  99. package/src/training/distillation/eval.d.ts +34 -0
  100. package/src/training/distillation/eval.js +310 -0
  101. package/src/training/distillation/index.d.ts +29 -0
  102. package/src/training/distillation/index.js +29 -0
  103. package/src/training/distillation/runtime.d.ts +20 -0
  104. package/src/training/distillation/runtime.js +121 -0
  105. package/src/training/distillation/scoreboard.d.ts +6 -0
  106. package/src/training/distillation/scoreboard.js +8 -0
  107. package/src/training/distillation/stage-a.d.ts +45 -0
  108. package/src/training/distillation/stage-a.js +338 -0
  109. package/src/training/distillation/stage-b.d.ts +24 -0
  110. package/src/training/distillation/stage-b.js +20 -0
  111. package/src/training/index.d.ts +10 -0
  112. package/src/training/index.js +10 -0
  113. package/src/training/lora-pipeline.d.ts +40 -0
  114. package/src/training/lora-pipeline.js +796 -0
  115. package/src/training/operator-artifacts.d.ts +62 -0
  116. package/src/training/operator-artifacts.js +140 -0
  117. package/src/training/operator-command.d.ts +5 -0
  118. package/src/training/operator-command.js +453 -0
  119. package/src/training/operator-eval.d.ts +48 -0
  120. package/src/training/operator-eval.js +230 -0
  121. package/src/training/operator-scoreboard.d.ts +5 -0
  122. package/src/training/operator-scoreboard.js +44 -0
  123. package/src/training/runner.d.ts +52 -0
  124. package/src/training/runner.js +29 -4
  125. package/src/training/suite.d.ts +112 -0
  126. package/src/training/suite.js +9 -9
  127. package/src/training/workloads.d.ts +164 -0
  128. package/src/training/workloads.js +539 -0
  129. package/src/version.js +1 -1
  130. package/tools/doppler-cli.js +137 -40
@@ -27,6 +27,24 @@
27
27
  "intent": "verify"
28
28
  }
29
29
  },
30
+ {
31
+ "match": {
32
+ "command": "lora"
33
+ },
34
+ "value": {
35
+ "suite": null,
36
+ "intent": null
37
+ }
38
+ },
39
+ {
40
+ "match": {
41
+ "command": "distill"
42
+ },
43
+ "value": {
44
+ "suite": null,
45
+ "intent": null
46
+ }
47
+ },
30
48
  {
31
49
  "match": {},
32
50
  "value": {
@@ -1,10 +1,12 @@
1
1
  import type { ConverterConfigSchema } from '../config/schema/converter.schema.js';
2
2
 
3
- export type ToolingCommand = 'convert' | 'debug' | 'bench' | 'verify';
3
+ export type ToolingCommand = 'convert' | 'debug' | 'bench' | 'verify' | 'lora' | 'distill';
4
4
  export type ToolingSurface = 'browser' | 'node';
5
5
  export type ToolingSuite = 'kernels' | 'inference' | 'training' | 'bench' | 'debug' | 'diffusion' | 'energy';
6
6
  export type ToolingIntent = 'verify' | 'investigate' | 'calibrate' | null;
7
7
  export type ToolingTrainingStage = 'stage1_joint' | 'stage2_base' | 'stage_a' | 'stage_b';
8
+ export type ToolingDistillAction = 'run' | 'stage-a' | 'stage-b' | 'eval' | 'watch' | 'compare' | 'quality-gate' | 'subsets';
9
+ export type ToolingLoraAction = 'run' | 'eval' | 'watch' | 'export' | 'compare' | 'quality-gate' | 'activate';
8
10
 
9
11
  export interface ToolingConvertExecutionPayload {
10
12
  workers?: number | null;
@@ -25,6 +27,7 @@ export interface ToolingConvertPayload {
25
27
 
26
28
  export interface ToolingCommandRequestInput {
27
29
  command: ToolingCommand;
30
+ action?: ToolingDistillAction | ToolingLoraAction;
28
31
  suite?: ToolingSuite;
29
32
  modelId?: string;
30
33
  trainingTests?: string[];
@@ -65,6 +68,17 @@ export interface ToolingCommandRequestInput {
65
68
  inputDir?: string;
66
69
  outputDir?: string;
67
70
  convertPayload?: ToolingConvertPayload;
71
+ workloadPath?: string;
72
+ runRoot?: string;
73
+ checkpointPath?: string;
74
+ checkpointId?: string;
75
+ checkpointStep?: number;
76
+ stageId?: string;
77
+ stageArtifact?: string;
78
+ subsetManifest?: string;
79
+ evalDatasetId?: string;
80
+ pollIntervalMs?: number;
81
+ stopWhenIdle?: boolean;
68
82
  captureOutput?: boolean;
69
83
  keepPipeline?: boolean;
70
84
  report?: Record<string, unknown> | null;
@@ -76,6 +90,7 @@ export interface ToolingCommandRequest {
76
90
  command: ToolingCommand;
77
91
  suite: ToolingSuite | null;
78
92
  intent: ToolingIntent;
93
+ action: ToolingDistillAction | ToolingLoraAction | null;
79
94
  modelId: string | null;
80
95
  trainingTests: string[] | null;
81
96
  trainingStage: ToolingTrainingStage | null;
@@ -115,6 +130,17 @@ export interface ToolingCommandRequest {
115
130
  inputDir: string | null;
116
131
  outputDir: string | null;
117
132
  convertPayload: ToolingConvertPayload | null;
133
+ workloadPath: string | null;
134
+ runRoot: string | null;
135
+ checkpointPath: string | null;
136
+ checkpointId: string | null;
137
+ checkpointStep: number | null;
138
+ stageId: string | null;
139
+ stageArtifact: string | null;
140
+ subsetManifest: string | null;
141
+ evalDatasetId: string | null;
142
+ pollIntervalMs: number | null;
143
+ stopWhenIdle: boolean | null;
118
144
  captureOutput: boolean;
119
145
  keepPipeline: boolean;
120
146
  report: Record<string, unknown> | null;
@@ -1,12 +1,14 @@
1
1
  import { isPlainObject } from '../utils/plain-object.js';
2
2
  import { selectRuleValue } from '../rules/rule-registry.js';
3
3
 
4
- const TOOLING_COMMAND_SET = ['convert', 'debug', 'bench', 'verify'];
4
+ const TOOLING_COMMAND_SET = ['convert', 'debug', 'bench', 'verify', 'lora', 'distill'];
5
5
  const TOOLING_SURFACE_SET = ['browser', 'node'];
6
6
  const TOOLING_SUITE_SET = ['kernels', 'inference', 'training', 'bench', 'debug', 'diffusion', 'energy'];
7
7
  const TOOLING_INTENT_SET = ['verify', 'investigate', 'calibrate'];
8
8
  const VERIFY_SUITES = ['kernels', 'inference', 'training', 'diffusion', 'energy'];
9
9
  const TRAINING_STAGE_SET = ['stage1_joint', 'stage2_base', 'stage_a', 'stage_b'];
10
+ const DISTILL_ACTION_SET = ['run', 'stage-a', 'stage-b', 'eval', 'watch', 'compare', 'quality-gate', 'subsets'];
11
+ const LORA_ACTION_SET = ['run', 'eval', 'watch', 'export', 'compare', 'quality-gate', 'activate'];
10
12
  const TRAINING_COMMAND_SCHEMA_VERSION = 1;
11
13
 
12
14
  export const TOOLING_COMMANDS = Object.freeze([...TOOLING_COMMAND_SET]);
@@ -82,6 +84,15 @@ function asOptionalForceResumeReason(value, label) {
82
84
  return reason;
83
85
  }
84
86
 
87
+ function asOptionalAction(value, label, allowed) {
88
+ const action = asOptionalString(value, label);
89
+ if (!action) return null;
90
+ if (!allowed.includes(action)) {
91
+ throw new Error(`tooling command: ${label} must be one of ${allowed.join(', ')}.`);
92
+ }
93
+ return action;
94
+ }
95
+
85
96
  function assertCommand(value) {
86
97
  const command = asOptionalString(value, 'command');
87
98
  if (!command) {
@@ -246,6 +257,7 @@ function normalizeConvert(raw) {
246
257
  command: 'convert',
247
258
  suite: null,
248
259
  intent: null,
260
+ action: null,
249
261
  modelId: null,
250
262
  trainingTests: null,
251
263
  trainingStage: null,
@@ -285,6 +297,113 @@ function normalizeConvert(raw) {
285
297
  inputDir,
286
298
  outputDir,
287
299
  convertPayload: payload,
300
+ workloadPath: null,
301
+ runRoot: null,
302
+ checkpointPath: null,
303
+ checkpointId: null,
304
+ checkpointStep: null,
305
+ stageId: null,
306
+ stageArtifact: null,
307
+ subsetManifest: null,
308
+ evalDatasetId: null,
309
+ pollIntervalMs: null,
310
+ stopWhenIdle: null,
311
+ captureOutput: false,
312
+ keepPipeline: false,
313
+ report: asOptionalObject(raw.report, 'report'),
314
+ timestamp: raw.timestamp ?? null,
315
+ searchParams: raw.searchParams ?? null,
316
+ };
317
+ }
318
+
319
+ function normalizeTrainingOperatorCommand(raw, command) {
320
+ const allowedActions = command === 'distill' ? DISTILL_ACTION_SET : LORA_ACTION_SET;
321
+ const action = asOptionalAction(raw.action, 'action', allowedActions);
322
+ if (!action) {
323
+ throw new Error(`tooling command: ${command} requires action.`);
324
+ }
325
+ const workloadPath = asOptionalString(raw.workloadPath, 'workloadPath');
326
+ const runRoot = asOptionalString(raw.runRoot, 'runRoot');
327
+ const checkpointPath = asOptionalString(raw.checkpointPath, 'checkpointPath');
328
+ const checkpointId = asOptionalString(raw.checkpointId, 'checkpointId');
329
+ const checkpointStep = asOptionalPositiveInteger(raw.checkpointStep, 'checkpointStep');
330
+ const stageId = asOptionalString(raw.stageId, 'stageId');
331
+ const stageArtifact = asOptionalString(raw.stageArtifact, 'stageArtifact');
332
+ const subsetManifest = asOptionalString(raw.subsetManifest, 'subsetManifest');
333
+ const evalDatasetId = asOptionalString(raw.evalDatasetId, 'evalDatasetId');
334
+ const pollIntervalMs = asOptionalPositiveInteger(raw.pollIntervalMs, 'pollIntervalMs');
335
+ const stopWhenIdle = asOptionalBoolean(raw.stopWhenIdle, 'stopWhenIdle');
336
+ if (!workloadPath && !runRoot) {
337
+ throw new Error(`tooling command: ${command} requires workloadPath or runRoot.`);
338
+ }
339
+ if ((action === 'eval' || action === 'export') && !checkpointPath && !runRoot) {
340
+ throw new Error(`tooling command: ${command} ${action} requires checkpointPath or runRoot.`);
341
+ }
342
+ if (action === 'watch' && !runRoot) {
343
+ throw new Error(`tooling command: ${command} watch requires runRoot.`);
344
+ }
345
+ if ((action === 'compare' || action === 'quality-gate') && !runRoot) {
346
+ throw new Error(`tooling command: ${command} ${action} requires runRoot.`);
347
+ }
348
+ if (command === 'distill' && action === 'stage-b' && !stageArtifact && !runRoot) {
349
+ throw new Error('tooling command: distill stage-b requires stageArtifact or runRoot.');
350
+ }
351
+
352
+ return {
353
+ command,
354
+ suite: null,
355
+ intent: null,
356
+ action,
357
+ modelId: null,
358
+ trainingTests: null,
359
+ trainingStage: null,
360
+ trainingConfig: null,
361
+ stage1Artifact: null,
362
+ stage1ArtifactHash: null,
363
+ ulArtifactDir: null,
364
+ stageAArtifact: null,
365
+ stageAArtifactHash: null,
366
+ distillArtifactDir: null,
367
+ teacherModelId: null,
368
+ studentModelId: null,
369
+ distillDatasetId: null,
370
+ distillDatasetPath: null,
371
+ distillLanguagePair: null,
372
+ distillSourceLangs: null,
373
+ distillTargetLangs: null,
374
+ distillPairAllowlist: null,
375
+ strictPairContract: null,
376
+ distillShardIndex: null,
377
+ distillShardCount: null,
378
+ resumeFrom: null,
379
+ forceResume: null,
380
+ forceResumeReason: null,
381
+ forceResumeSource: null,
382
+ checkpointOperator: null,
383
+ trainingSchemaVersion: null,
384
+ trainingBenchSteps: null,
385
+ checkpointEvery: null,
386
+ workloadType: 'training',
387
+ modelUrl: null,
388
+ cacheMode: asOptionalCacheMode(raw.cacheMode, 'cacheMode'),
389
+ loadMode: asOptionalLoadMode(raw.loadMode, 'loadMode'),
390
+ runtimePreset: asOptionalString(raw.runtimePreset, 'runtimePreset'),
391
+ runtimeConfigUrl: asOptionalString(raw.runtimeConfigUrl, 'runtimeConfigUrl'),
392
+ runtimeConfig: asOptionalObject(raw.runtimeConfig, 'runtimeConfig'),
393
+ inputDir: null,
394
+ outputDir: null,
395
+ convertPayload: null,
396
+ workloadPath,
397
+ runRoot,
398
+ checkpointPath,
399
+ checkpointId,
400
+ checkpointStep,
401
+ stageId,
402
+ stageArtifact,
403
+ subsetManifest,
404
+ evalDatasetId,
405
+ pollIntervalMs,
406
+ stopWhenIdle,
288
407
  captureOutput: false,
289
408
  keepPipeline: false,
290
409
  report: asOptionalObject(raw.report, 'report'),
@@ -428,6 +547,7 @@ function normalizeSuiteCommand(raw, command) {
428
547
  command,
429
548
  suite,
430
549
  intent: runtimeContract.intent,
550
+ action: null,
431
551
  modelId,
432
552
  trainingTests,
433
553
  trainingStage,
@@ -469,6 +589,17 @@ function normalizeSuiteCommand(raw, command) {
469
589
  inputDir: null,
470
590
  outputDir: null,
471
591
  convertPayload: null,
592
+ workloadPath: null,
593
+ runRoot: null,
594
+ checkpointPath: null,
595
+ checkpointId: null,
596
+ checkpointStep: null,
597
+ stageId: null,
598
+ stageArtifact: null,
599
+ subsetManifest: null,
600
+ evalDatasetId: null,
601
+ pollIntervalMs: null,
602
+ stopWhenIdle: null,
472
603
  captureOutput: asOptionalBoolean(raw.captureOutput, 'captureOutput') ?? false,
473
604
  keepPipeline: asOptionalBoolean(raw.keepPipeline, 'keepPipeline') ?? false,
474
605
  report: asOptionalObject(raw.report, 'report'),
@@ -485,6 +616,9 @@ export function normalizeToolingCommandRequest(input) {
485
616
  if (command === 'convert') {
486
617
  return normalizeConvert(input);
487
618
  }
619
+ if (command === 'lora' || command === 'distill') {
620
+ return normalizeTrainingOperatorCommand(input, command);
621
+ }
488
622
  return normalizeSuiteCommand(input, command);
489
623
  }
490
624
 
@@ -514,8 +648,13 @@ export function ensureCommandSupportedOnSurface(commandRequest, surface) {
514
648
  throw new Error(`tooling command: unsupported surface "${surface}".`);
515
649
  }
516
650
 
517
- // All commands are contractually available on both surfaces.
518
- // Surface-specific capability checks happen in the runners.
651
+ if (
652
+ normalizedSurface === 'browser'
653
+ && (request.command === 'lora' || request.command === 'distill')
654
+ ) {
655
+ throw new Error(`tooling command: ${request.command} is currently Node-only and must fail closed on browser.`);
656
+ }
657
+
519
658
  return {
520
659
  request,
521
660
  surface: normalizedSurface,
@@ -7,6 +7,10 @@ import type { BrowserCommandRunResult } from './browser-command-runner.js';
7
7
 
8
8
  export interface NodeBrowserCommandRunOptions {
9
9
  staticRootDir?: string;
10
+ staticMounts?: Array<{
11
+ urlPrefix: string;
12
+ rootDir: string;
13
+ }>;
10
14
  baseUrl?: string;
11
15
  host?: string;
12
16
  port?: number;
@@ -67,8 +67,61 @@ function resolveStaticPath(rootDir, requestPath) {
67
67
  return candidate;
68
68
  }
69
69
 
70
- async function resolveFileForRequest(rootDir, requestPath) {
71
- const resolved = resolveStaticPath(rootDir, requestPath);
70
+ function normalizeStaticMounts(mounts = []) {
71
+ if (!Array.isArray(mounts)) {
72
+ throw new Error('browser command: staticMounts must be an array.');
73
+ }
74
+
75
+ return mounts.map((mount, index) => {
76
+ if (!mount || typeof mount !== 'object' || Array.isArray(mount)) {
77
+ throw new Error(`browser command: staticMounts[${index}] must be an object.`);
78
+ }
79
+ const urlPrefix = String(mount.urlPrefix || '').trim();
80
+ const rootDir = String(mount.rootDir || '').trim();
81
+ if (!urlPrefix.startsWith('/')) {
82
+ throw new Error(`browser command: staticMounts[${index}].urlPrefix must start with "/".`);
83
+ }
84
+ if (!rootDir) {
85
+ throw new Error(`browser command: staticMounts[${index}].rootDir is required.`);
86
+ }
87
+ return {
88
+ urlPrefix: urlPrefix.replace(/\/+$/u, '') || '/',
89
+ rootDir: path.resolve(rootDir),
90
+ };
91
+ });
92
+ }
93
+
94
+ function findStaticRootForRequest(rootDir, mounts, requestPath) {
95
+ const normalizedPath = String(requestPath || '/');
96
+ let bestMount = null;
97
+
98
+ for (const mount of mounts) {
99
+ const prefix = mount.urlPrefix;
100
+ if (normalizedPath !== prefix && !normalizedPath.startsWith(`${prefix}/`)) {
101
+ continue;
102
+ }
103
+ if (!bestMount || prefix.length > bestMount.urlPrefix.length) {
104
+ bestMount = mount;
105
+ }
106
+ }
107
+
108
+ if (!bestMount) {
109
+ return {
110
+ effectiveRootDir: rootDir,
111
+ effectivePath: normalizedPath,
112
+ };
113
+ }
114
+
115
+ const relativePath = normalizedPath.slice(bestMount.urlPrefix.length) || '/';
116
+ return {
117
+ effectiveRootDir: bestMount.rootDir,
118
+ effectivePath: relativePath.startsWith('/') ? relativePath : `/${relativePath}`,
119
+ };
120
+ }
121
+
122
+ async function resolveFileForRequest(rootDir, mounts, requestPath) {
123
+ const { effectiveRootDir, effectivePath } = findStaticRootForRequest(rootDir, mounts, requestPath);
124
+ const resolved = resolveStaticPath(effectiveRootDir, effectivePath);
72
125
  if (!resolved) return null;
73
126
 
74
127
  let stats;
@@ -99,6 +152,7 @@ async function createStaticFileServer(options = {}) {
99
152
  const rootDir = path.resolve(
100
153
  options.rootDir || fileURLToPath(new URL('../../', import.meta.url))
101
154
  );
155
+ const staticMounts = normalizeStaticMounts(options.staticMounts || []);
102
156
  const host = String(options.host || DEFAULT_HOST);
103
157
  const port = Number.isFinite(options.port) ? Math.max(0, Math.floor(options.port)) : 0;
104
158
 
@@ -120,7 +174,7 @@ async function createStaticFileServer(options = {}) {
120
174
  return;
121
175
  }
122
176
 
123
- const resolved = await resolveFileForRequest(rootDir, pathname);
177
+ const resolved = await resolveFileForRequest(rootDir, staticMounts, pathname);
124
178
  if (!resolved) {
125
179
  res.statusCode = 404;
126
180
  res.end('File not found');
@@ -545,6 +599,7 @@ export async function runBrowserCommandInNode(commandRequest, options = {}) {
545
599
  ? null
546
600
  : await createStaticFileServer({
547
601
  rootDir: options.staticRootDir,
602
+ staticMounts: options.staticMounts,
548
603
  host: options.host,
549
604
  port: serverPort,
550
605
  }).catch((error) => {
@@ -18,6 +18,7 @@ import {
18
18
  getActiveKernelPathSource,
19
19
  setActiveKernelPath,
20
20
  } from '../config/kernel-path-loader.js';
21
+ import { runTrainingOperatorCommand } from '../training/operator-command.js';
21
22
 
22
23
  function asOptionalPlainObject(value, label) {
23
24
  if (value == null) return null;
@@ -90,6 +91,20 @@ export async function runNodeCommand(commandRequest, options = {}) {
90
91
  });
91
92
  }
92
93
 
94
+ if (request.command === 'lora' || request.command === 'distill') {
95
+ const gpuOptionalActions = new Set(['compare', 'quality-gate', 'subsets']);
96
+ installNodeFileFetchShim();
97
+ if (!gpuOptionalActions.has(request.action)) {
98
+ await assertNodeWebGPUSupport();
99
+ }
100
+ const result = await runTrainingOperatorCommand(request);
101
+ return createToolingSuccessEnvelope({
102
+ surface: 'node',
103
+ request,
104
+ result,
105
+ });
106
+ }
107
+
93
108
  await assertNodeWebGPUSupport();
94
109
  const modules = await loadRuntimeModules();
95
110
  const runtimeBridge = {
@@ -2,17 +2,6 @@ import { existsSync, readFileSync, statSync } from 'node:fs';
2
2
  import { dirname, isAbsolute, resolve } from 'node:path';
3
3
  import { fileURLToPath, pathToFileURL } from 'node:url';
4
4
 
5
- const DOPPLER_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
6
-
7
- const DEFAULT_LOCAL_DOE_PROVIDER_PATH = resolve(
8
- dirname(fileURLToPath(import.meta.url)),
9
- '..',
10
- '..',
11
- '..',
12
- 'fawn',
13
- 'nursery',
14
- 'webgpu-doe',
15
- );
16
5
  const DEFAULT_DOE_PROVIDER_CREATE_ARGS = 'enable-dawn-features=allow_unsafe_apis';
17
6
 
18
7
  function hasNavigatorGpu() {
@@ -59,20 +48,7 @@ function resolveCandidateModuleSpecifier(candidate) {
59
48
  }
60
49
 
61
50
  function resolveDefaultWebgpuModuleSpecifiers() {
62
- const specifiers = [];
63
- const localCandidates = [
64
- resolve(process.cwd(), '..', 'fawn', 'nursery', 'webgpu-doe'),
65
- DEFAULT_LOCAL_DOE_PROVIDER_PATH,
66
- ];
67
- for (const localCandidate of localCandidates) {
68
- const resolvedPath = resolveNodeModuleFilePath(localCandidate);
69
- if (resolvedPath) {
70
- specifiers.push(pathToFileURL(resolvedPath).href);
71
- }
72
- }
73
- specifiers.push('@simulatte/webgpu-doe');
74
- specifiers.push('webgpu');
75
- return [...new Set(specifiers)];
51
+ return ['@simulatte/webgpu', 'webgpu'];
76
52
  }
77
53
 
78
54
  function resolveWebgpuModuleSpecifiers() {
@@ -89,80 +65,26 @@ function resolveWebgpuModuleSpecifiers() {
89
65
  };
90
66
  }
91
67
 
92
- function resolveWorkspaceWebgpuProviderPath() {
93
- const candidates = [
94
- resolve(process.cwd(), 'node_modules', 'webgpu'),
95
- resolve(DOPPLER_ROOT, 'node_modules', 'webgpu'),
96
- ];
97
- for (const candidate of candidates) {
98
- const resolvedPath = resolveNodeModuleFilePath(candidate);
99
- if (resolvedPath) {
100
- return resolvedPath;
101
- }
102
- }
103
- return null;
104
- }
105
-
106
68
  function isDoeWebgpuSpecifier(specifier) {
107
- if (specifier === '@simulatte/webgpu-doe') {
108
- return true;
109
- }
110
- if (typeof specifier !== 'string') {
111
- return false;
112
- }
113
- if (specifier.includes('/webgpu-doe/')) {
69
+ if (specifier === '@simulatte/webgpu') {
114
70
  return true;
115
71
  }
116
- return specifier.includes('webgpu-doe') && specifier.startsWith('file://');
117
- }
118
-
119
- function resolveDoeProviderOverride(specifier) {
120
- const explicitProvider = process.env.FAWN_WEBGPU_NODE_PROVIDER_MODULE;
121
- if (typeof explicitProvider === 'string' && explicitProvider.trim().length > 0) {
122
- return null;
123
- }
124
- if (!isDoeWebgpuSpecifier(specifier)) {
125
- return null;
126
- }
127
- return resolveWorkspaceWebgpuProviderPath();
72
+ return typeof specifier === 'string'
73
+ && specifier.startsWith('file://')
74
+ && specifier.includes('@simulatte/webgpu');
128
75
  }
129
76
 
130
77
  async function importWithProviderOverride(specifier) {
131
- const providerOverride = resolveDoeProviderOverride(specifier);
132
78
  const shouldApplyCreateArgsDefault = isDoeWebgpuSpecifier(specifier)
133
79
  && !(typeof process.env.FAWN_WEBGPU_CREATE_ARGS === 'string' && process.env.FAWN_WEBGPU_CREATE_ARGS.trim().length > 0);
134
- if (!providerOverride) {
135
- if (!shouldApplyCreateArgsDefault) {
136
- return import(specifier);
137
- }
138
- process.env.FAWN_WEBGPU_CREATE_ARGS = DEFAULT_DOE_PROVIDER_CREATE_ARGS;
139
- try {
140
- return await import(specifier);
141
- } finally {
142
- delete process.env.FAWN_WEBGPU_CREATE_ARGS;
143
- }
144
- }
145
- const hadProvider = Object.prototype.hasOwnProperty.call(process.env, 'FAWN_WEBGPU_NODE_PROVIDER_MODULE');
146
- const previousProvider = process.env.FAWN_WEBGPU_NODE_PROVIDER_MODULE;
147
- const hadCreateArgs = Object.prototype.hasOwnProperty.call(process.env, 'FAWN_WEBGPU_CREATE_ARGS');
148
- const previousCreateArgs = process.env.FAWN_WEBGPU_CREATE_ARGS;
149
- process.env.FAWN_WEBGPU_NODE_PROVIDER_MODULE = providerOverride;
150
- if (!hadCreateArgs) {
151
- process.env.FAWN_WEBGPU_CREATE_ARGS = DEFAULT_DOE_PROVIDER_CREATE_ARGS;
80
+ if (!shouldApplyCreateArgsDefault) {
81
+ return import(specifier);
152
82
  }
83
+ process.env.FAWN_WEBGPU_CREATE_ARGS = DEFAULT_DOE_PROVIDER_CREATE_ARGS;
153
84
  try {
154
85
  return await import(specifier);
155
86
  } finally {
156
- if (hadProvider) {
157
- process.env.FAWN_WEBGPU_NODE_PROVIDER_MODULE = previousProvider;
158
- } else {
159
- delete process.env.FAWN_WEBGPU_NODE_PROVIDER_MODULE;
160
- }
161
- if (hadCreateArgs) {
162
- process.env.FAWN_WEBGPU_CREATE_ARGS = previousCreateArgs;
163
- } else {
164
- delete process.env.FAWN_WEBGPU_CREATE_ARGS;
165
- }
87
+ delete process.env.FAWN_WEBGPU_CREATE_ARGS;
166
88
  }
167
89
  }
168
90
 
@@ -0,0 +1,7 @@
1
+ export declare function watchFinalizedCheckpoints(options: {
2
+ checkpointsDir: string;
3
+ manifestPath: string;
4
+ pollIntervalMs?: number | null;
5
+ stopWhenIdle?: boolean;
6
+ onCheckpoint: (markerPath: string) => Promise<void> | void;
7
+ }): Promise<{ ok: true; processedCount: number; manifestPath: string }>;
@@ -0,0 +1,106 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+
4
+ import { writeJsonArtifact } from './operator-artifacts.js';
5
+
6
+ async function listCheckpointMarkers(checkpointsDir) {
7
+ const absoluteDir = resolve(String(checkpointsDir));
8
+ const entries = await readdir(absoluteDir, { withFileTypes: true });
9
+ const markers = [];
10
+ for (const entry of entries) {
11
+ if (!entry.isDirectory()) {
12
+ continue;
13
+ }
14
+ const entryPath = join(absoluteDir, entry.name);
15
+ const markerPath = join(entryPath, 'checkpoint.complete.json');
16
+ try {
17
+ await readFile(markerPath, 'utf8');
18
+ markers.push(markerPath);
19
+ continue;
20
+ } catch (error) {
21
+ if (error?.code !== 'ENOENT') {
22
+ throw error;
23
+ }
24
+ }
25
+ markers.push(...await listCheckpointMarkers(entryPath));
26
+ }
27
+ return markers.sort((left, right) => left.localeCompare(right));
28
+ }
29
+
30
+ async function ensureDirectoryExists(directoryPath) {
31
+ try {
32
+ const entries = await readdir(directoryPath, { withFileTypes: true });
33
+ return Array.isArray(entries);
34
+ } catch (error) {
35
+ if (error?.code === 'ENOENT') {
36
+ return false;
37
+ }
38
+ throw error;
39
+ }
40
+ }
41
+
42
+ async function readProcessedManifest(manifestPath) {
43
+ try {
44
+ const raw = await readFile(manifestPath, 'utf8');
45
+ const parsed = JSON.parse(raw);
46
+ const processed = Array.isArray(parsed?.processedCheckpointMarkers)
47
+ ? parsed.processedCheckpointMarkers.filter((entry) => typeof entry === 'string')
48
+ : [];
49
+ return new Set(processed);
50
+ } catch (error) {
51
+ if (error?.code === 'ENOENT') {
52
+ return new Set();
53
+ }
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ export async function watchFinalizedCheckpoints(options) {
59
+ const checkpointsDir = resolve(String(options.checkpointsDir));
60
+ const manifestPath = resolve(String(options.manifestPath));
61
+ const pollIntervalMs = Number.isFinite(options.pollIntervalMs)
62
+ ? Math.max(100, Math.floor(options.pollIntervalMs))
63
+ : 2000;
64
+ const stopWhenIdle = options.stopWhenIdle === true;
65
+ const onCheckpoint = typeof options.onCheckpoint === 'function'
66
+ ? options.onCheckpoint
67
+ : null;
68
+ if (!onCheckpoint) {
69
+ throw new Error('watchFinalizedCheckpoints requires onCheckpoint(markerPath).');
70
+ }
71
+
72
+ const processed = await readProcessedManifest(manifestPath);
73
+ let idlePolls = 0;
74
+ for (;;) {
75
+ const checkpointsExist = await ensureDirectoryExists(checkpointsDir);
76
+ const markers = checkpointsExist
77
+ ? await listCheckpointMarkers(checkpointsDir)
78
+ : [];
79
+ let sawNewMarker = false;
80
+ for (const markerPath of markers) {
81
+ if (processed.has(markerPath)) continue;
82
+ sawNewMarker = true;
83
+ await onCheckpoint(markerPath);
84
+ processed.add(markerPath);
85
+ await writeJsonArtifact(manifestPath, {
86
+ artifactType: 'training_checkpoint_watch_manifest',
87
+ schemaVersion: 1,
88
+ generatedAt: new Date().toISOString(),
89
+ processedCheckpointMarkers: [...processed].sort((left, right) => left.localeCompare(right)),
90
+ });
91
+ }
92
+ if (!sawNewMarker) {
93
+ idlePolls += 1;
94
+ if (stopWhenIdle && idlePolls > 0) {
95
+ return {
96
+ ok: true,
97
+ processedCount: processed.size,
98
+ manifestPath,
99
+ };
100
+ }
101
+ } else {
102
+ idlePolls = 0;
103
+ }
104
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, pollIntervalMs));
105
+ }
106
+ }
@@ -23,7 +23,12 @@ export declare function saveCheckpoint(
23
23
  key: string,
24
24
  data: unknown,
25
25
  options?: CheckpointStoreOptions
26
- ): Promise<void>;
26
+ ): Promise<{
27
+ key: string;
28
+ path: string | null;
29
+ metadata: Record<string, unknown>;
30
+ data: unknown;
31
+ }>;
27
32
 
28
33
  export declare function loadCheckpoint(
29
34
  key: string,