nebula-notebook 0.2.15 → 0.2.16
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/assets/{errorwidget-B2XZ3hoa.js → errorwidget-DfcjRDKM.js} +1 -1
- package/dist/assets/{index-CV0ZPmTH.js → index-COAUb-T8.js} +1 -1
- package/dist/assets/{index-DqHzn4vg.js → index-CyyuPG9B.js} +1 -1
- package/dist/assets/{index-a9gKLTlv.js → index-DrhpYYGt.js} +1 -1
- package/dist/assets/{index-NfxQwPBq.js → index-LZ-zzybU.js} +43 -43
- package/dist/assets/{services-shim-D_SeYcB9.js → services-shim-CCJm9nl7.js} +1 -1
- package/dist/index.html +1 -1
- package/node-server/dist/cluster/kernel-proxy.d.ts +15 -0
- package/node-server/dist/cluster/kernel-proxy.js +75 -0
- package/node-server/dist/index.js +2 -0
- package/node-server/dist/notebook/headless-handler.d.ts +10 -0
- package/node-server/dist/notebook/headless-handler.js +64 -10
- package/node-server/dist/scheduler/allocation-service.d.ts +3 -2
- package/node-server/dist/scheduler/allocation-service.js +45 -2
- package/node-server/dist/scheduler/arch.d.ts +30 -0
- package/node-server/dist/scheduler/arch.js +113 -0
- package/node-server/dist/scheduler/job-template.d.ts +11 -0
- package/node-server/dist/scheduler/mock-scheduler.d.ts +1 -0
- package/node-server/dist/scheduler/mock-scheduler.js +3 -0
- package/node-server/dist/scheduler/slurm-scheduler.d.ts +1 -0
- package/node-server/dist/scheduler/slurm-scheduler.js +34 -0
- package/node-server/dist/scheduler/types.d.ts +9 -0
- package/node-server/dist/scheduler/util.d.ts +6 -0
- package/node-server/dist/scheduler/util.js +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Multi-arch launch support for compute allocations.
|
|
4
|
+
*
|
|
5
|
+
* An allocation re-launches THIS Nebula install on the compute node, so the
|
|
6
|
+
* node binary and node_modules must match the compute node's CPU arch. On
|
|
7
|
+
* heterogeneous clusters (CRI: x86_64 login nodes, aarch64 ghq/pearsonq) a
|
|
8
|
+
* per-arch runtime is configured via env:
|
|
9
|
+
*
|
|
10
|
+
* NEBULA_ARM64_NODE_BIN=/shared/node22-arm64/bin/node
|
|
11
|
+
* NEBULA_ARM64_DIR=/shared/nebula-notebook-arm64 # checkout with arm64 node_modules
|
|
12
|
+
*
|
|
13
|
+
* Both must live on storage the compute nodes share with the login node.
|
|
14
|
+
*/
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.normalizeArch = normalizeArch;
|
|
50
|
+
exports.archOverridesFromEnv = archOverridesFromEnv;
|
|
51
|
+
exports.pickLaunchContext = pickLaunchContext;
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
/** SLURM spellings (uname -m) → node's process.arch identifiers. */
|
|
54
|
+
function normalizeArch(raw) {
|
|
55
|
+
const a = (raw || '').trim().toLowerCase();
|
|
56
|
+
if (a === 'x86_64' || a === 'amd64' || a === 'x64')
|
|
57
|
+
return 'x64';
|
|
58
|
+
if (a === 'aarch64' || a === 'arm64')
|
|
59
|
+
return 'arm64';
|
|
60
|
+
return a;
|
|
61
|
+
}
|
|
62
|
+
const CONFIGURABLE_ARCHES = ['arm64', 'x64'];
|
|
63
|
+
/**
|
|
64
|
+
* Read per-arch runtime overrides from the environment. An arch is configured
|
|
65
|
+
* only when BOTH its vars are present — a node binary without its matching
|
|
66
|
+
* node_modules tree (or vice versa) would just fail later and worse.
|
|
67
|
+
*/
|
|
68
|
+
function archOverridesFromEnv(env = process.env) {
|
|
69
|
+
const overrides = {};
|
|
70
|
+
for (const arch of CONFIGURABLE_ARCHES) {
|
|
71
|
+
const prefix = `NEBULA_${arch.toUpperCase()}_`;
|
|
72
|
+
const nodeBin = env[`${prefix}NODE_BIN`]?.trim();
|
|
73
|
+
const dir = env[`${prefix}DIR`]?.trim();
|
|
74
|
+
if (!nodeBin || !dir)
|
|
75
|
+
continue;
|
|
76
|
+
overrides[arch] = {
|
|
77
|
+
nodeBin,
|
|
78
|
+
cwd: path.join(dir, 'node-server'),
|
|
79
|
+
scriptPath: path.join(dir, 'node-server', 'src', 'index.ts'),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return overrides;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Pick the launch context for a partition's arch: the server's own install
|
|
86
|
+
* when arches match (or the arch is unknown — the status quo), the configured
|
|
87
|
+
* override when they differ, and a loud, actionable refusal otherwise. The
|
|
88
|
+
* refusal is the point: without it the job dies on the compute node with
|
|
89
|
+
* "Exec format error" in a log nobody reads.
|
|
90
|
+
*/
|
|
91
|
+
function pickLaunchContext(ctx, targetArchRaw, serverArch = process.arch // undefined → the running process's arch
|
|
92
|
+
) {
|
|
93
|
+
if (!targetArchRaw)
|
|
94
|
+
return ctx;
|
|
95
|
+
const target = normalizeArch(targetArchRaw);
|
|
96
|
+
if (target === normalizeArch(serverArch))
|
|
97
|
+
return ctx;
|
|
98
|
+
const override = ctx.archOverrides?.[target];
|
|
99
|
+
if (!override) {
|
|
100
|
+
const prefix = `NEBULA_${target.toUpperCase()}_`;
|
|
101
|
+
throw new Error(`this partition runs ${targetArchRaw} nodes but this Nebula server is ${serverArch} — ` +
|
|
102
|
+
`its runtime cannot execute there. Install a ${targetArchRaw} Node.js and Nebula checkout ` +
|
|
103
|
+
`on shared storage and set ${prefix}NODE_BIN and ${prefix}DIR on the server.`);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
...ctx,
|
|
107
|
+
nodeBin: override.nodeBin,
|
|
108
|
+
cwd: override.cwd,
|
|
109
|
+
scriptPath: override.scriptPath,
|
|
110
|
+
// Never let a client restart itself mid-job on a source change.
|
|
111
|
+
execArgv: ctx.execArgv.filter((a) => a !== '--watch'),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -26,5 +26,16 @@ export interface LaunchContext {
|
|
|
26
26
|
cwd: string;
|
|
27
27
|
/** Directory (on shared storage) for job scripts + logs */
|
|
28
28
|
stateDir: string;
|
|
29
|
+
/**
|
|
30
|
+
* Per-arch runtime substitutions (node's process.arch keys, e.g. 'arm64')
|
|
31
|
+
* for partitions whose CPU arch differs from this server's — see arch.ts.
|
|
32
|
+
*/
|
|
33
|
+
archOverrides?: Record<string, {
|
|
34
|
+
nodeBin: string;
|
|
35
|
+
cwd: string;
|
|
36
|
+
scriptPath: string;
|
|
37
|
+
}>;
|
|
38
|
+
/** Arch of nodeBin itself. Defaults to process.arch; overridable for tests. */
|
|
39
|
+
serverArch?: string;
|
|
29
40
|
}
|
|
30
41
|
export declare function renderJobScript(spec: JobSpec, ctx: LaunchContext, allocId: string, token: string): string;
|
|
@@ -21,6 +21,7 @@ export declare class MockScheduler implements Scheduler {
|
|
|
21
21
|
associations(_user: string): Promise<Associations>;
|
|
22
22
|
load(): Promise<QueueLoad>;
|
|
23
23
|
allowedQos(partition: string): Promise<string[] | null>;
|
|
24
|
+
partitionArch(_partition: string): Promise<string | null>;
|
|
24
25
|
estimateStart(): Promise<StartEstimate>;
|
|
25
26
|
submit(scriptPath: string): Promise<{
|
|
26
27
|
jobId: string;
|
|
@@ -73,6 +73,9 @@ class MockScheduler {
|
|
|
73
73
|
return ['priority', 'opportunistic'];
|
|
74
74
|
return null;
|
|
75
75
|
}
|
|
76
|
+
async partitionArch(_partition) {
|
|
77
|
+
return null; // mock cluster is arch-homogeneous
|
|
78
|
+
}
|
|
76
79
|
async estimateStart() {
|
|
77
80
|
// The launcher uses capacity-based availability, not this dry-run estimate.
|
|
78
81
|
return {};
|
|
@@ -21,6 +21,7 @@ export declare class SlurmScheduler implements Scheduler {
|
|
|
21
21
|
private associationsFresh;
|
|
22
22
|
load(): Promise<QueueLoad>;
|
|
23
23
|
private loadFresh;
|
|
24
|
+
partitionArch(partition: string): Promise<string | null>;
|
|
24
25
|
allowedQos(partition: string): Promise<string[] | null>;
|
|
25
26
|
estimateStart(spec: JobSpec): Promise<StartEstimate>;
|
|
26
27
|
submit(scriptPath: string): Promise<{
|
|
@@ -11,6 +11,7 @@ exports.SlurmScheduler = void 0;
|
|
|
11
11
|
const child_process_1 = require("child_process");
|
|
12
12
|
const util_1 = require("util");
|
|
13
13
|
const util_2 = require("./util");
|
|
14
|
+
const arch_1 = require("./arch");
|
|
14
15
|
const execFileP = (0, util_1.promisify)(child_process_1.execFile);
|
|
15
16
|
async function run(cmd, args, timeoutMs = 15_000) {
|
|
16
17
|
const { stdout, stderr } = await execFileP(cmd, args, {
|
|
@@ -210,7 +211,26 @@ class SlurmScheduler {
|
|
|
210
211
|
// GPU capacity per partition, from per-node TRES: configured (CfgTRES) vs
|
|
211
212
|
// allocated (AllocTRES) `gres/gpu`, so we can report *idle* (available) GPUs
|
|
212
213
|
// rather than a per-node count. Generic — no site-specific node/gres names.
|
|
214
|
+
// The same per-node dump also carries each node's CPU arch (`Arch=`), which
|
|
215
|
+
// multi-arch clusters (x86_64 login + aarch64 queues) need at submit time.
|
|
213
216
|
if (nodeRes.status === 'fulfilled') {
|
|
217
|
+
const archAgg = new Map();
|
|
218
|
+
for (const line of nodeRes.value.stdout.split('\n')) {
|
|
219
|
+
if (!line.trim())
|
|
220
|
+
continue;
|
|
221
|
+
const arch = scontrolField(line, 'Arch');
|
|
222
|
+
const nodeParts = scontrolField(line, 'Partitions');
|
|
223
|
+
if (arch && nodeParts) {
|
|
224
|
+
for (const part of nodeParts.split(',')) {
|
|
225
|
+
(archAgg.get(part) ?? archAgg.set(part, new Set()).get(part)).add(arch);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (const [part, archs] of archAgg) {
|
|
230
|
+
const p = partitions.get(part);
|
|
231
|
+
if (p)
|
|
232
|
+
p.archs = [...archs].sort();
|
|
233
|
+
}
|
|
214
234
|
// Aggregate per (partition, GPU model) — heterogeneous queues mix cards.
|
|
215
235
|
const agg = new Map();
|
|
216
236
|
for (const line of nodeRes.value.stdout.split('\n')) {
|
|
@@ -307,6 +327,20 @@ class SlurmScheduler {
|
|
|
307
327
|
}
|
|
308
328
|
return { partitions: [...partitions.values()], qoses, fetchedAt: Date.now() };
|
|
309
329
|
}
|
|
330
|
+
async partitionArch(partition) {
|
|
331
|
+
try {
|
|
332
|
+
const load = await this.load();
|
|
333
|
+
const archs = load.partitions.find((p) => p.name === partition)?.archs;
|
|
334
|
+
// Mixed-arch partitions have no safe pick — the scheduler decides the
|
|
335
|
+
// node, so treat them as unknown rather than guessing wrong half the time.
|
|
336
|
+
if (!archs || archs.length !== 1)
|
|
337
|
+
return null;
|
|
338
|
+
return (0, arch_1.normalizeArch)(archs[0]);
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
310
344
|
async allowedQos(partition) {
|
|
311
345
|
const cached = this.qosCache.get(partition);
|
|
312
346
|
if (cached && Date.now() - cached.at < 300_000)
|
|
@@ -70,6 +70,9 @@ export interface PartitionLoad {
|
|
|
70
70
|
pending: number;
|
|
71
71
|
running: number;
|
|
72
72
|
};
|
|
73
|
+
/** Distinct CPU arches of the partition's nodes, SLURM spelling (e.g.
|
|
74
|
+
* 'x86_64', 'aarch64'), sorted. Absent when no node reported one. */
|
|
75
|
+
archs?: string[];
|
|
73
76
|
}
|
|
74
77
|
export interface QosLoad {
|
|
75
78
|
name: string;
|
|
@@ -105,6 +108,12 @@ export interface Scheduler {
|
|
|
105
108
|
* scheduler would reject. Discovered from the scheduler, not configured.
|
|
106
109
|
*/
|
|
107
110
|
allowedQos(partition: string): Promise<string[] | null>;
|
|
111
|
+
/**
|
|
112
|
+
* The partition's CPU arch as a node process.arch identifier ('x64',
|
|
113
|
+
* 'arm64'), or null when unknown or mixed — null means "assume the
|
|
114
|
+
* server's own arch", preserving pre-multi-arch behavior.
|
|
115
|
+
*/
|
|
116
|
+
partitionArch(partition: string): Promise<string | null>;
|
|
108
117
|
/** Dry-run estimated start time for a spec, without submitting. */
|
|
109
118
|
estimateStart(spec: JobSpec): Promise<StartEstimate>;
|
|
110
119
|
/** Submit a rendered job script; returns the scheduler job id. */
|
|
@@ -5,3 +5,9 @@
|
|
|
5
5
|
export declare function formatWalltime(minutes: number): string;
|
|
6
6
|
/** POSIX single-quote a string so it is safe to embed in a shell script. */
|
|
7
7
|
export declare function shellQuote(s: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Compress a job log's tail into a one-line failure reason: the last few
|
|
10
|
+
* non-empty lines (the error is at the end), capped so a pathological log
|
|
11
|
+
* can't flood the UI. Null when there is nothing to say.
|
|
12
|
+
*/
|
|
13
|
+
export declare function summarizeLogTail(content: string): string | null;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.formatWalltime = formatWalltime;
|
|
7
7
|
exports.shellQuote = shellQuote;
|
|
8
|
+
exports.summarizeLogTail = summarizeLogTail;
|
|
8
9
|
/** Format minutes as a SLURM walltime string (`D-HH:MM:SS` or `HH:MM:SS`). */
|
|
9
10
|
function formatWalltime(minutes) {
|
|
10
11
|
const total = Math.max(1, Math.floor(minutes));
|
|
@@ -18,3 +19,18 @@ function formatWalltime(minutes) {
|
|
|
18
19
|
function shellQuote(s) {
|
|
19
20
|
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
20
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Compress a job log's tail into a one-line failure reason: the last few
|
|
24
|
+
* non-empty lines (the error is at the end), capped so a pathological log
|
|
25
|
+
* can't flood the UI. Null when there is nothing to say.
|
|
26
|
+
*/
|
|
27
|
+
function summarizeLogTail(content) {
|
|
28
|
+
const lines = content
|
|
29
|
+
.split('\n')
|
|
30
|
+
.map((l) => l.trimEnd())
|
|
31
|
+
.filter((l) => l.trim().length > 0);
|
|
32
|
+
if (lines.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
const joined = lines.slice(-3).join(' | ');
|
|
35
|
+
return joined.length > 300 ? `…${joined.slice(-300)}` : joined;
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nebula-notebook",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"description": "AI-native notebook computing environment — real Jupyter kernels, real filesystem, built to be driven by agents (Claude Code / Codex) via MCP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|