@mcmcjs/julia 0.3.0 → 0.5.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/dist/index.js CHANGED
@@ -52,8 +52,7 @@ var juliaEngine = {
52
52
  };
53
53
 
54
54
  // src/fit.ts
55
- import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "fs";
56
- import { tmpdir } from "os";
55
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
57
56
  import { join as join3 } from "path";
58
57
  import {
59
58
  canonicalJson,
@@ -63,11 +62,19 @@ import {
63
62
 
64
63
  // src/runner-common.ts
65
64
  import { createHash } from "crypto";
65
+ import { tmpdir } from "os";
66
66
  import { dirname, join as join2 } from "path";
67
67
  import { fileURLToPath } from "url";
68
+ function sharedTmpParent() {
69
+ const uid = typeof process.getuid === "function" ? `-${process.getuid()}` : "";
70
+ return join2(tmpdir(), `mcmcjs${uid}`);
71
+ }
68
72
  function driverPath() {
69
73
  return join2(dirname(fileURLToPath(import.meta.url)), "driver.jl");
70
74
  }
75
+ function workerPath() {
76
+ return join2(dirname(fileURLToPath(import.meta.url)), "worker.jl");
77
+ }
71
78
  function sha256(text) {
72
79
  return createHash("sha256").update(text).digest("hex");
73
80
  }
@@ -89,45 +96,40 @@ var STAGES = /* @__PURE__ */ new Set([
89
96
  "load_samples",
90
97
  "predict",
91
98
  "write",
92
- "spawn"
99
+ "spawn",
100
+ "worker"
93
101
  ]);
94
102
  function toStage(value) {
95
103
  return typeof value === "string" && STAGES.has(value) ? value : void 0;
96
104
  }
97
105
 
98
106
  // src/fit.ts
99
- async function runFit(spec, resolved, io) {
100
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
101
- const start = performance.now();
107
+ function makeRequestDir(kind) {
108
+ const parent = sharedTmpParent();
109
+ mkdirSync(parent, { recursive: true });
110
+ return mkdtempSync(join3(parent, `${kind}-`));
111
+ }
112
+ function fitRequest(spec, outPath) {
113
+ return {
114
+ schema_version: spec.schema_version,
115
+ backend: { id: spec.backend.id },
116
+ model: { file: spec.modelPath, entry: spec.model.entry },
117
+ data: spec.data,
118
+ sampler: spec.sampler,
119
+ seed: spec.seed,
120
+ out: outPath
121
+ };
122
+ }
123
+ function finalizeOkFit(spec, resolved, io, provenance, startedAt, elapsedMs) {
102
124
  const runtimeRequested = spec.backend.version;
103
- const tmp = io.tmpDir ?? mkdtempSync(join3(tmpdir(), "mcmcjs-fit-"));
104
- const requestPath = join3(tmp, "request.json");
105
- writeFileSync(
106
- requestPath,
107
- JSON.stringify({
108
- schema_version: spec.schema_version,
109
- backend: { id: spec.backend.id },
110
- model: { file: spec.modelPath, entry: spec.model.entry },
111
- data: spec.data,
112
- sampler: spec.sampler,
113
- seed: spec.seed,
114
- out: io.outPath
115
- })
116
- );
117
- const args = [
118
- ...resolved.args,
119
- "--startup-file=no",
120
- `--project=${io.projectDir}`,
121
- driverPath(),
122
- requestPath
123
- ];
124
- const { stdout, stderr, code } = await io.spawn(resolved.command, args);
125
- const elapsedMs = Math.round(performance.now() - start);
126
- if (code !== 0 || !existsSync(io.outPath)) {
127
- const failure = lastJsonLine(stderr);
128
- const stage = toStage(failure?.stage);
129
- const error = failure?.error ?? stderr.trim().split("\n").pop() ?? `julia exited with code ${code}`;
130
- return { status: "error", runtimeRequested, elapsedMs, stage, error };
125
+ if (!existsSync(io.outPath)) {
126
+ return {
127
+ status: "error",
128
+ runtimeRequested,
129
+ elapsedMs,
130
+ stage: "write",
131
+ error: "the runtime reported success but wrote no samples file"
132
+ };
131
133
  }
132
134
  try {
133
135
  parseSamples(readFileSync(io.outPath, "utf8"));
@@ -140,7 +142,6 @@ async function runFit(spec, resolved, io) {
140
142
  error: `samples file did not parse: ${error.message}`
141
143
  };
142
144
  }
143
- const provenance = lastJsonLine(stdout);
144
145
  const julia = provenance?.julia_version;
145
146
  const record = {
146
147
  schema_version: RUN_RECORD_SCHEMA_VERSION,
@@ -156,7 +157,7 @@ async function runFit(spec, resolved, io) {
156
157
  started_at: startedAt,
157
158
  elapsed_ms: elapsedMs
158
159
  };
159
- writeFileSync(`${io.outPath}.run.json`, `${JSON.stringify(record, null, 2)}
160
+ writeFileSync(io.recordPath ?? `${io.outPath}.run.json`, `${JSON.stringify(record, null, 2)}
160
161
  `);
161
162
  return {
162
163
  status: "ok",
@@ -166,10 +167,43 @@ async function runFit(spec, resolved, io) {
166
167
  elapsedMs
167
168
  };
168
169
  }
170
+ async function runFit(spec, resolved, io) {
171
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
172
+ const start = performance.now();
173
+ const runtimeRequested = spec.backend.version;
174
+ const ownTmp = io.tmpDir === void 0;
175
+ const tmp = io.tmpDir ?? makeRequestDir("fit");
176
+ const requestPath = join3(tmp, "request.json");
177
+ writeFileSync(requestPath, JSON.stringify(fitRequest(spec, io.outPath)));
178
+ const args = [
179
+ ...resolved.args,
180
+ "--startup-file=no",
181
+ `--project=${io.projectDir}`,
182
+ driverPath(),
183
+ requestPath
184
+ ];
185
+ let stdout;
186
+ let stderr;
187
+ let code;
188
+ try {
189
+ ({ stdout, stderr, code } = await io.spawn(resolved.command, args, {
190
+ onProgress: io.onProgress
191
+ }));
192
+ } finally {
193
+ if (ownTmp) rmSync(tmp, { recursive: true, force: true });
194
+ }
195
+ const elapsedMs = Math.round(performance.now() - start);
196
+ if (code !== 0 || !existsSync(io.outPath)) {
197
+ const failure = lastJsonLine(stderr);
198
+ const stage = toStage(failure?.stage);
199
+ const error = failure?.error ?? stderr.trim().split("\n").pop() ?? `julia exited with code ${code}`;
200
+ return { status: "error", runtimeRequested, elapsedMs, stage, error };
201
+ }
202
+ return finalizeOkFit(spec, resolved, io, lastJsonLine(stdout), startedAt, elapsedMs);
203
+ }
169
204
 
170
205
  // src/predict.ts
171
- import { existsSync as existsSync2, mkdtempSync as mkdtempSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
172
- import { tmpdir as tmpdir2 } from "os";
206
+ import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
173
207
  import { join as join4 } from "path";
174
208
  import {
175
209
  canonicalJson as canonicalJson2,
@@ -191,7 +225,8 @@ async function runPredict(spec, resolved, io) {
191
225
  const start = performance.now();
192
226
  const runtimeRequested = spec.backend.version;
193
227
  const data = predictData(spec);
194
- const tmp = io.tmpDir ?? mkdtempSync2(join4(tmpdir2(), "mcmcjs-predict-"));
228
+ const ownTmp = io.tmpDir === void 0;
229
+ const tmp = io.tmpDir ?? makeRequestDir("predict");
195
230
  const requestPath = join4(tmp, "request.json");
196
231
  writeFileSync2(
197
232
  requestPath,
@@ -214,7 +249,14 @@ async function runPredict(spec, resolved, io) {
214
249
  driverPath(),
215
250
  requestPath
216
251
  ];
217
- const { stdout, stderr, code } = await io.spawn(resolved.command, args);
252
+ let stdout;
253
+ let stderr;
254
+ let code;
255
+ try {
256
+ ({ stdout, stderr, code } = await io.spawn(resolved.command, args));
257
+ } finally {
258
+ if (ownTmp) rmSync2(tmp, { recursive: true, force: true });
259
+ }
218
260
  const elapsedMs = Math.round(performance.now() - start);
219
261
  if (code !== 0 || !existsSync2(io.outPath)) {
220
262
  const failure = lastJsonLine(stderr);
@@ -262,12 +304,14 @@ async function runPredict(spec, resolved, io) {
262
304
  }
263
305
 
264
306
  // src/project.ts
265
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
307
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
266
308
  import { homedir as homedir2 } from "os";
267
309
  import { join as join5 } from "path";
268
310
  import { createRunner as createRunner3 } from "@mcmcjs/engine";
269
311
  var PACKAGES = [
270
312
  "Turing",
313
+ "FlexiChains",
314
+ "DimensionalData",
271
315
  "MCMCChains",
272
316
  "JuliaBUGS",
273
317
  "AdvancedHMC",
@@ -294,7 +338,7 @@ function managedProjectReady(dir = managedProjectDir()) {
294
338
  }
295
339
  async function ensureProject(juliaBin, run = createRunner3(30 * 6e4), dir = managedProjectDir()) {
296
340
  if (managedProjectReady(dir)) return dir;
297
- mkdirSync(dir, { recursive: true });
341
+ mkdirSync2(dir, { recursive: true });
298
342
  const code = `using Pkg; Pkg.add(${JSON.stringify(PACKAGES)}); Pkg.precompile()`;
299
343
  await run(juliaBin, ["--startup-file=no", `--project=${dir}`, "-e", code]);
300
344
  writeFileSync3(sentinelPath(dir), JSON.stringify([...PACKAGES].sort()));
@@ -462,7 +506,7 @@ async function resolveVersion(bin, channel, run) {
462
506
  `Julia version "${channel}" is not installed. Add it with: mcmc julia version add ${channel}`
463
507
  );
464
508
  }
465
- return { command: match.File, args: match.Args ?? [] };
509
+ return { command: match.File, args: match.Args ?? [], version: match.Version };
466
510
  }
467
511
  function assertVersionsInstalled(channels, installed) {
468
512
  const have = new Set(installed.map((v) => v.id));
@@ -474,6 +518,256 @@ function assertVersionsInstalled(channels, installed) {
474
518
  );
475
519
  }
476
520
  }
521
+
522
+ // src/worker.ts
523
+ import { spawn } from "child_process";
524
+ import { mkdirSync as mkdirSync3, readdirSync, rmdirSync, rmSync as rmSync3, statSync } from "fs";
525
+ import { createConnection } from "net";
526
+ import { join as join7 } from "path";
527
+ import { parseProgressLine } from "@mcmcjs/engine";
528
+ var START_WAIT_MS = 18e4;
529
+ var CONNECT_TIMEOUT_MS = 1e3;
530
+ var STOP_ACK_TIMEOUT_MS = 3e3;
531
+ function fitInactivityTimeoutMs() {
532
+ const fromEnv = Number(process.env.MCMC_WORKER_TIMEOUT_MS);
533
+ return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : 30 * 6e4;
534
+ }
535
+ var WorkerTimeoutError = class extends Error {
536
+ };
537
+ function workersDir() {
538
+ const runtime = process.env.XDG_RUNTIME_DIR;
539
+ const dir = runtime ? join7(runtime, "mcmcjs") : join7(sharedTmpParent(), "workers");
540
+ mkdirSync3(dir, { recursive: true, mode: 448 });
541
+ return dir;
542
+ }
543
+ function workerSocketPath(command, projectDir) {
544
+ return join7(workersDir(), `worker-${sha256(`${command}
545
+ ${projectDir}`).slice(0, 12)}.sock`);
546
+ }
547
+ function connectOnce(path) {
548
+ return new Promise((resolve, reject) => {
549
+ const sock = createConnection(path);
550
+ const timer = setTimeout(() => {
551
+ sock.destroy();
552
+ reject(new Error("connect timeout"));
553
+ }, CONNECT_TIMEOUT_MS);
554
+ sock.once("connect", () => {
555
+ clearTimeout(timer);
556
+ resolve(sock);
557
+ });
558
+ sock.once("error", (error) => {
559
+ clearTimeout(timer);
560
+ reject(error);
561
+ });
562
+ });
563
+ }
564
+ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
565
+ function acquireStartLock(socketPath) {
566
+ const lock = `${socketPath}.start`;
567
+ try {
568
+ mkdirSync3(lock);
569
+ return true;
570
+ } catch {
571
+ try {
572
+ if (Date.now() - statSync(lock).mtimeMs > START_WAIT_MS) {
573
+ rmdirSync(lock);
574
+ mkdirSync3(lock);
575
+ return true;
576
+ }
577
+ } catch {
578
+ }
579
+ return false;
580
+ }
581
+ }
582
+ function releaseStartLock(socketPath) {
583
+ try {
584
+ rmdirSync(`${socketPath}.start`);
585
+ } catch {
586
+ }
587
+ }
588
+ async function ensureWorker(resolved, projectDir, notify) {
589
+ const path = workerSocketPath(resolved.command, projectDir);
590
+ try {
591
+ return await connectOnce(path);
592
+ } catch {
593
+ }
594
+ let spawnFailure;
595
+ const startedHere = acquireStartLock(path);
596
+ if (startedHere) {
597
+ try {
598
+ rmSync3(path, { force: true });
599
+ notify?.("starting the Julia worker (first use loads Turing, can take a minute)...");
600
+ const child = spawn(
601
+ resolved.command,
602
+ [...resolved.args, "--startup-file=no", `--project=${projectDir}`, workerPath(), path],
603
+ { detached: true, stdio: "ignore" }
604
+ );
605
+ child.on("error", (error) => {
606
+ spawnFailure = error;
607
+ });
608
+ child.on("exit", (code) => {
609
+ if (code !== 0 && code !== null) {
610
+ spawnFailure = new Error(`the worker exited with code ${code} before serving`);
611
+ }
612
+ });
613
+ child.unref();
614
+ } catch (error) {
615
+ releaseStartLock(path);
616
+ throw error;
617
+ }
618
+ }
619
+ try {
620
+ const deadline = Date.now() + START_WAIT_MS;
621
+ for (; ; ) {
622
+ if (spawnFailure) throw spawnFailure;
623
+ try {
624
+ return await connectOnce(path);
625
+ } catch {
626
+ }
627
+ if (Date.now() > deadline) throw new Error("the Julia worker did not start in time");
628
+ await delay(250);
629
+ }
630
+ } finally {
631
+ if (startedHere) releaseStartLock(path);
632
+ }
633
+ }
634
+ function readWorkerResponse(sock, onProgress, inactivityMs = fitInactivityTimeoutMs()) {
635
+ return new Promise((resolve, reject) => {
636
+ let pending = "";
637
+ let settled = false;
638
+ const settle = (fn) => {
639
+ if (settled) return;
640
+ settled = true;
641
+ sock.setTimeout(0);
642
+ fn();
643
+ };
644
+ sock.setTimeout(inactivityMs, () => {
645
+ const error = new WorkerTimeoutError(
646
+ `the worker sent nothing for ${Math.round(inactivityMs / 6e4)} minutes`
647
+ );
648
+ settle(() => {
649
+ sock.destroy();
650
+ reject(error);
651
+ });
652
+ });
653
+ sock.on("data", (chunk) => {
654
+ pending += chunk.toString("utf8");
655
+ for (; ; ) {
656
+ const at = pending.indexOf("\n");
657
+ if (at === -1) break;
658
+ const line = pending.slice(0, at).trim();
659
+ pending = pending.slice(at + 1);
660
+ if (!line) continue;
661
+ const progress = parseProgressLine(line);
662
+ if (progress) {
663
+ onProgress?.(progress);
664
+ continue;
665
+ }
666
+ try {
667
+ const doc = JSON.parse(line);
668
+ settle(() => {
669
+ sock.end();
670
+ resolve(doc);
671
+ });
672
+ return;
673
+ } catch {
674
+ }
675
+ }
676
+ });
677
+ sock.on("error", (error) => settle(() => reject(error)));
678
+ sock.on("close", () => settle(() => reject(new Error("worker closed the connection"))));
679
+ });
680
+ }
681
+ async function runFitViaWorker(spec, resolved, io) {
682
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
683
+ const start = performance.now();
684
+ const sock = await ensureWorker(resolved, io.projectDir, io.notify);
685
+ sock.write(`${JSON.stringify(fitRequest(spec, io.outPath))}
686
+ `);
687
+ const final = await readWorkerResponse(sock, io.onProgress);
688
+ const elapsedMs = Math.round(performance.now() - start);
689
+ if (final.ok !== true) {
690
+ if (final.stage === "worker") throw new Error(String(final.error ?? "worker error"));
691
+ return {
692
+ status: "error",
693
+ runtimeRequested: spec.backend.version,
694
+ elapsedMs,
695
+ stage: toStage(final.stage),
696
+ error: String(final.error ?? "worker error")
697
+ };
698
+ }
699
+ return finalizeOkFit(
700
+ spec,
701
+ resolved,
702
+ io,
703
+ final.provenance,
704
+ startedAt,
705
+ elapsedMs
706
+ );
707
+ }
708
+ async function runFitAuto(spec, resolved, io) {
709
+ if (io.daemon && process.platform !== "win32") {
710
+ const start = performance.now();
711
+ try {
712
+ return await runFitViaWorker(spec, resolved, io);
713
+ } catch (error) {
714
+ if (error instanceof WorkerTimeoutError) {
715
+ return {
716
+ status: "error",
717
+ runtimeRequested: spec.backend.version,
718
+ elapsedMs: Math.round(performance.now() - start),
719
+ stage: "worker",
720
+ error: `${error.message}; the worker may be wedged, try mcmc daemon stop`
721
+ };
722
+ }
723
+ io.notify?.(
724
+ `worker unavailable (${error.message}); falling back to a one-shot run`
725
+ );
726
+ }
727
+ }
728
+ return runFit(spec, resolved, io);
729
+ }
730
+ async function listWorkers() {
731
+ const dir = workersDir();
732
+ const sockets = readdirSync(dir).filter((f) => f.startsWith("worker-") && f.endsWith(".sock"));
733
+ const statuses = [];
734
+ for (const name of sockets) {
735
+ const socket = join7(dir, name);
736
+ let alive = false;
737
+ try {
738
+ const sock = await connectOnce(socket);
739
+ sock.destroy();
740
+ alive = true;
741
+ } catch {
742
+ }
743
+ statuses.push({ socket, alive });
744
+ }
745
+ return statuses;
746
+ }
747
+ async function stopWorker(socket) {
748
+ let sock;
749
+ try {
750
+ sock = await connectOnce(socket);
751
+ } catch {
752
+ rmSync3(socket, { force: true });
753
+ return "stale";
754
+ }
755
+ const acked = await new Promise((resolve) => {
756
+ const timer = setTimeout(() => resolve(false), STOP_ACK_TIMEOUT_MS);
757
+ const finish = (ok) => {
758
+ clearTimeout(timer);
759
+ resolve(ok);
760
+ };
761
+ sock.resume();
762
+ sock.on("close", () => finish(true));
763
+ sock.on("error", () => finish(false));
764
+ sock.write('{"mcmcjs":"stop"}\n');
765
+ });
766
+ sock.destroy();
767
+ if (!acked) return "pending";
768
+ rmSync3(socket, { force: true });
769
+ return "stopped";
770
+ }
477
771
  export {
478
772
  addVersion,
479
773
  assertVersionsInstalled,
@@ -484,6 +778,7 @@ export {
484
778
  juliaEngine,
485
779
  juliaupInstallCommand,
486
780
  listVersions,
781
+ listWorkers,
487
782
  managedProjectDir,
488
783
  managedProjectReady,
489
784
  planSetup,
@@ -492,10 +787,13 @@ export {
492
787
  resolveVersion,
493
788
  runDoctor,
494
789
  runFit,
790
+ runFitAuto,
495
791
  runMatrix,
496
792
  runPredict,
497
793
  runSetup,
498
794
  setDefaultVersion,
795
+ sharedTmpParent,
796
+ stopWorker,
499
797
  updateVersion
500
798
  };
501
799
  //# sourceMappingURL=index.js.map