@nosana/node 1.1.33-rc → 1.1.34-rc
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/package.json +1 -1
- package/dist/src/NodeManager/node/resource/definition/index.d.ts +1 -1
- package/dist/src/NodeManager/node/resource/definition/index.js +1 -1
- package/dist/src/NodeManager/node/resource/resourceManager.d.ts +2 -2
- package/dist/src/NodeManager/node/resource/resourceManager.js +2 -2
- package/dist/src/NodeManager/node/resource/volume/volumeManager.d.ts +34 -6
- package/dist/src/NodeManager/node/resource/volume/volumeManager.js +46 -23
- package/dist/src/NodeManager/provider/Provider.js +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/dist/package.json
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const s3HelperImage = "registry.hub.docker.com/nosana/resource-manager:1.1.
|
|
1
|
+
export declare const s3HelperImage = "registry.hub.docker.com/nosana/resource-manager:1.1.12";
|
|
2
2
|
export declare const nosanaBucket = "https://models.nosana.io";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const s3HelperImage = 'registry.hub.docker.com/nosana/resource-manager:1.1.
|
|
1
|
+
export const s3HelperImage = 'registry.hub.docker.com/nosana/resource-manager:1.1.12';
|
|
2
2
|
export const nosanaBucket = 'https://models.nosana.io';
|
|
@@ -3,7 +3,7 @@ import { Resource } from '@nosana/sdk/dist/types/resources.js';
|
|
|
3
3
|
import { ContainerOrchestrationInterface } from '../../provider/containerOrchestration/interface.js';
|
|
4
4
|
import { NodeRepository } from '../../repository/NodeRepository.js';
|
|
5
5
|
import { ImageManager } from './image/imageManager.js';
|
|
6
|
-
import { VolumeManager } from './volume/volumeManager.js';
|
|
6
|
+
import { JobContext, VolumeManager } from './volume/volumeManager.js';
|
|
7
7
|
/**
|
|
8
8
|
* The market endpoint reports an HF resource's model as `url`, the field it
|
|
9
9
|
* also uses for S3, while everywhere else in the node — job definitions
|
|
@@ -24,7 +24,7 @@ export declare class ResourceManager {
|
|
|
24
24
|
resyncResourcesDB(): Promise<void>;
|
|
25
25
|
fetchMarketRequiredResources(market: Market | string): Promise<void>;
|
|
26
26
|
prune(): Promise<void>;
|
|
27
|
-
getResourceVolumes(resources: Resource[], controller: AbortController): Promise<{
|
|
27
|
+
getResourceVolumes(resources: Resource[], controller: AbortController, job?: JobContext): Promise<{
|
|
28
28
|
dest: string;
|
|
29
29
|
name: string;
|
|
30
30
|
readonly?: boolean;
|
|
@@ -53,10 +53,10 @@ export class ResourceManager {
|
|
|
53
53
|
await this.images.pruneImages();
|
|
54
54
|
await this.volumes.pruneVolumes();
|
|
55
55
|
}
|
|
56
|
-
async getResourceVolumes(resources, controller) {
|
|
56
|
+
async getResourceVolumes(resources, controller, job) {
|
|
57
57
|
const volumes = [];
|
|
58
58
|
for (const resource of resources) {
|
|
59
|
-
await this.volumes.createRemoteVolume(resource, controller);
|
|
59
|
+
await this.volumes.createRemoteVolume(resource, controller, job);
|
|
60
60
|
if ((await this.volumes.hasVolume(resource)) === false) {
|
|
61
61
|
const error = new Error(`Missing required resource ${createResourceName(resource)}.`);
|
|
62
62
|
throw error;
|
|
@@ -1,17 +1,45 @@
|
|
|
1
|
+
import { ContainerInspectInfo } from 'dockerode';
|
|
1
2
|
import { Resource } from '@nosana/sdk/dist/types/resources.js';
|
|
2
3
|
import { RequiredResource } from '@nosana/sdk';
|
|
3
4
|
import { ContainerOrchestrationInterface } from '../../../provider/containerOrchestration/interface.js';
|
|
4
5
|
import { NodeRepository } from '../../../repository/NodeRepository.js';
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
+
* Renders the runtime state that says why the download stopped, for inclusion in
|
|
8
|
+
* the error message. A process that is killed rather than failing logs nothing,
|
|
9
|
+
* so this is the only evidence of an OOM; a container the runtime could not
|
|
10
|
+
* create or start logs nothing either, and `State.Error` is Docker's only
|
|
11
|
+
* account of that. The run duration separates a download that died on contact
|
|
12
|
+
* from one the network dropped an hour in.
|
|
13
|
+
*
|
|
14
|
+
* Optional because the caller inspects on a best-effort basis; the fields
|
|
15
|
+
* themselves are always present when it succeeds.
|
|
16
|
+
*/
|
|
17
|
+
export declare function describeContainerState(state?: ContainerInspectInfo['State']): string;
|
|
18
|
+
/**
|
|
19
|
+
* The reasons the resource manager gave for failing, if it gave any.
|
|
7
20
|
*
|
|
8
21
|
* It reports failures as a JSON line on stderr — `{"event":"error","message":
|
|
9
|
-
* "..."}` — from every service it implements, then exits non-zero.
|
|
10
|
-
*
|
|
22
|
+
* "..."}` — from every service it implements, then exits non-zero. Parallel
|
|
23
|
+
* download workers can each report their own failure before the run dies, so
|
|
24
|
+
* every such line is collected, in the order logged; the last is the one the
|
|
25
|
+
* run gave up on.
|
|
11
26
|
*/
|
|
12
|
-
export declare function
|
|
27
|
+
export declare function findReportedErrors(logs: {
|
|
13
28
|
log?: string;
|
|
14
|
-
}[]): string
|
|
29
|
+
}[]): string[];
|
|
30
|
+
/**
|
|
31
|
+
* A resource rendered for an error message. Resources carry credentials (`IAM`,
|
|
32
|
+
* HF `accessToken`), and errors reach the logs and the support channel, so
|
|
33
|
+
* those fields are replaced rather than reported.
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* The op a download is running for. Passed only for job resources — a market
|
|
37
|
+
* preload has no op state to write errors to.
|
|
38
|
+
*/
|
|
39
|
+
export interface JobContext {
|
|
40
|
+
id: string;
|
|
41
|
+
opIndex: number;
|
|
42
|
+
}
|
|
15
43
|
export declare class VolumeManager {
|
|
16
44
|
private containerOrchestration;
|
|
17
45
|
private repository;
|
|
@@ -20,7 +48,7 @@ export declare class VolumeManager {
|
|
|
20
48
|
private progressBarReporter;
|
|
21
49
|
constructor(containerOrchestration: ContainerOrchestrationInterface, repository: NodeRepository);
|
|
22
50
|
pullMarketRequiredVolumes(remoteResources: RequiredResource[]): Promise<void>;
|
|
23
|
-
createRemoteVolume(resource: RequiredResource, controller: AbortController): Promise<string>;
|
|
51
|
+
createRemoteVolume(resource: RequiredResource, controller: AbortController, job?: JobContext): Promise<string>;
|
|
24
52
|
private runResourceManagerContainer;
|
|
25
53
|
setVolume(bucket: string, volume: string): Promise<void>;
|
|
26
54
|
hasVolume(resource: Resource): Promise<boolean>;
|
|
@@ -11,48 +11,60 @@ import { createHFArgs } from '../helpers/createHFArgs.js';
|
|
|
11
11
|
import { createS3Args } from '../helpers/createS3Args.js';
|
|
12
12
|
import { createOllamaArgs } from '../helpers/createOllamaArgs.js';
|
|
13
13
|
import { liveAbortSignal } from '../../../utils/liveAbortSignal.js';
|
|
14
|
+
import { formatTime } from '../../../utils/formatTime.js';
|
|
14
15
|
/**
|
|
15
16
|
* Renders the runtime state that says why the download stopped, for inclusion in
|
|
16
17
|
* the error message. A process that is killed rather than failing logs nothing,
|
|
17
|
-
* so this is the only evidence of an OOM
|
|
18
|
+
* so this is the only evidence of an OOM; a container the runtime could not
|
|
19
|
+
* create or start logs nothing either, and `State.Error` is Docker's only
|
|
20
|
+
* account of that. The run duration separates a download that died on contact
|
|
21
|
+
* from one the network dropped an hour in.
|
|
18
22
|
*
|
|
19
23
|
* Optional because the caller inspects on a best-effort basis; the fields
|
|
20
24
|
* themselves are always present when it succeeds.
|
|
21
25
|
*/
|
|
22
|
-
function describeContainerState(state) {
|
|
26
|
+
export function describeContainerState(state) {
|
|
23
27
|
if (!state)
|
|
24
28
|
return '';
|
|
25
29
|
const parts = [
|
|
26
30
|
`exitCode: ${state.ExitCode}`,
|
|
27
31
|
`OOMKilled: ${state.OOMKilled}`
|
|
28
32
|
];
|
|
33
|
+
if (state.Error) {
|
|
34
|
+
parts.push(`error: ${state.Error}`);
|
|
35
|
+
}
|
|
36
|
+
const started = Date.parse(state.StartedAt);
|
|
37
|
+
const finished = Date.parse(state.FinishedAt);
|
|
38
|
+
// Docker reports a container that never started or never finished with the
|
|
39
|
+
// zero timestamp "0001-01-01T00:00:00Z"; a duration across one is noise.
|
|
40
|
+
if (started > 0 && finished >= started) {
|
|
41
|
+
parts.push(`ran for ${formatTime((finished - started) / 1000)}`);
|
|
42
|
+
}
|
|
29
43
|
return ` [${parts.join(', ')}]`;
|
|
30
44
|
}
|
|
31
45
|
/**
|
|
32
|
-
* The
|
|
46
|
+
* The reasons the resource manager gave for failing, if it gave any.
|
|
33
47
|
*
|
|
34
48
|
* It reports failures as a JSON line on stderr — `{"event":"error","message":
|
|
35
|
-
* "..."}` — from every service it implements, then exits non-zero.
|
|
36
|
-
*
|
|
49
|
+
* "..."}` — from every service it implements, then exits non-zero. Parallel
|
|
50
|
+
* download workers can each report their own failure before the run dies, so
|
|
51
|
+
* every such line is collected, in the order logged; the last is the one the
|
|
52
|
+
* run gave up on.
|
|
37
53
|
*/
|
|
38
|
-
export function
|
|
39
|
-
|
|
54
|
+
export function findReportedErrors(logs) {
|
|
55
|
+
const messages = [];
|
|
56
|
+
for (const log of logs) {
|
|
40
57
|
if (log.log?.includes('{"event":"error"')) {
|
|
41
58
|
try {
|
|
42
59
|
const parsed = JSON.parse(log.log.slice(log.log.indexOf('{')));
|
|
43
60
|
if (typeof parsed.message === 'string')
|
|
44
|
-
|
|
61
|
+
messages.push(parsed.message);
|
|
45
62
|
}
|
|
46
63
|
catch { }
|
|
47
64
|
}
|
|
48
65
|
}
|
|
49
|
-
return
|
|
66
|
+
return messages;
|
|
50
67
|
}
|
|
51
|
-
/**
|
|
52
|
-
* A resource rendered for an error message. Resources carry credentials (`IAM`,
|
|
53
|
-
* HF `accessToken`), and errors reach the logs and the support channel, so
|
|
54
|
-
* those fields are replaced rather than reported.
|
|
55
|
-
*/
|
|
56
68
|
function describeResource(resource) {
|
|
57
69
|
return JSON.stringify(resource, (key, value) => key === 'IAM' || key === 'accessToken' ? '[hidden]' : value);
|
|
58
70
|
}
|
|
@@ -83,7 +95,7 @@ export class VolumeManager {
|
|
|
83
95
|
}
|
|
84
96
|
}
|
|
85
97
|
}
|
|
86
|
-
async createRemoteVolume(resource, controller) {
|
|
98
|
+
async createRemoteVolume(resource, controller, job) {
|
|
87
99
|
const resourceName = createResourceName(resource);
|
|
88
100
|
// Checked before anything is allocated. Left to the download, an empty repo
|
|
89
101
|
// becomes a request for `/api/models//tree/main` and comes back as a bare
|
|
@@ -121,12 +133,12 @@ export class VolumeManager {
|
|
|
121
133
|
files: s3Resource.files,
|
|
122
134
|
bucket: s3Resource.bucket,
|
|
123
135
|
}, s3Resource.IAM);
|
|
124
|
-
await this.runResourceManagerContainer(volumeName, resourceName, args, controller, sync);
|
|
136
|
+
await this.runResourceManagerContainer(volumeName, resourceName, args, controller, sync, job);
|
|
125
137
|
}
|
|
126
138
|
else {
|
|
127
139
|
for (const bucket of s3Resource.buckets) {
|
|
128
140
|
const args = createS3Args(volumeName, { url: bucket.url, files: bucket.files }, s3Resource.IAM);
|
|
129
|
-
await this.runResourceManagerContainer(volumeName, resourceName, args, controller, sync);
|
|
141
|
+
await this.runResourceManagerContainer(volumeName, resourceName, args, controller, sync, job);
|
|
130
142
|
}
|
|
131
143
|
}
|
|
132
144
|
this.setVolume(resourceName, volumeName);
|
|
@@ -139,7 +151,7 @@ export class VolumeManager {
|
|
|
139
151
|
try {
|
|
140
152
|
const { repo, revision, files: hfFiles, accessToken, } = resource;
|
|
141
153
|
const args = createHFArgs(volumeName, { repo, revision, files: hfFiles }, accessToken);
|
|
142
|
-
await this.runResourceManagerContainer(volumeName, resourceName, args, controller, sync);
|
|
154
|
+
await this.runResourceManagerContainer(volumeName, resourceName, args, controller, sync, job);
|
|
143
155
|
this.setVolume(resourceName, volumeName);
|
|
144
156
|
}
|
|
145
157
|
catch (err) {
|
|
@@ -150,7 +162,7 @@ export class VolumeManager {
|
|
|
150
162
|
try {
|
|
151
163
|
const { model } = resource;
|
|
152
164
|
const args = createOllamaArgs(volumeName, model);
|
|
153
|
-
await this.runResourceManagerContainer(volumeName, resourceName, args, controller, sync);
|
|
165
|
+
await this.runResourceManagerContainer(volumeName, resourceName, args, controller, sync, job);
|
|
154
166
|
this.setVolume(resourceName, volumeName);
|
|
155
167
|
}
|
|
156
168
|
catch (err) {
|
|
@@ -160,7 +172,7 @@ export class VolumeManager {
|
|
|
160
172
|
}
|
|
161
173
|
return volumeName;
|
|
162
174
|
}
|
|
163
|
-
async runResourceManagerContainer(volume, name, args, controller, syncing = false) {
|
|
175
|
+
async runResourceManagerContainer(volume, name, args, controller, syncing = false, job) {
|
|
164
176
|
const container = await this.containerOrchestration.runContainer(args);
|
|
165
177
|
try {
|
|
166
178
|
const logStream = await container.logs({
|
|
@@ -214,13 +226,24 @@ export class VolumeManager {
|
|
|
214
226
|
.inspect()
|
|
215
227
|
.then(({ State }) => State)
|
|
216
228
|
.catch(() => undefined);
|
|
229
|
+
const reported = findReportedErrors(logs);
|
|
230
|
+
// Written straight to the op state: the summary thrown below quotes
|
|
231
|
+
// only the last reported failure, and parallel download workers can
|
|
232
|
+
// each report their own before the run dies.
|
|
233
|
+
if (job) {
|
|
234
|
+
for (const message of reported) {
|
|
235
|
+
this.repository.updateOpStateError(job.id, job.opIndex, {
|
|
236
|
+
event: 'resource-error',
|
|
237
|
+
message,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
217
241
|
// A non-zero exit is a failed download whether or not the container
|
|
218
242
|
// managed to report a reason, so this must throw either way: returning
|
|
219
243
|
// normally here reports success for an unfinished volume, and the run
|
|
220
244
|
// then fails later with the cause lost.
|
|
221
|
-
const reason =
|
|
222
|
-
|
|
223
|
-
throw new Error(`${reason}${describeContainerState(state)}`);
|
|
245
|
+
const reason = reported.at(-1) ?? `no reason reported (exit code ${StatusCode})`;
|
|
246
|
+
throw new Error(`resource download failed for ${name}: ${reason}${describeContainerState(state)}`);
|
|
224
247
|
}
|
|
225
248
|
}
|
|
226
249
|
catch (error) {
|
|
@@ -213,7 +213,7 @@ export class Provider {
|
|
|
213
213
|
await this.containerOrchestration.pullImage(s3HelperImage, undefined, controller);
|
|
214
214
|
this.resourceManager.images.setImage(s3HelperImage);
|
|
215
215
|
// transformCollections has already resolved any SpreadMarker objects by this point
|
|
216
|
-
const resourceVolumes = await this.resourceManager.getResourceVolumes(op.args.resources ?? [], controller);
|
|
216
|
+
const resourceVolumes = await this.resourceManager.getResourceVolumes(op.args.resources ?? [], controller, { id: flow.id, opIndex: index });
|
|
217
217
|
volumes.push(...resourceVolumes);
|
|
218
218
|
}
|
|
219
219
|
// The op only declares that it wants a TEE; the node's configured
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nosana/node",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.34-rc",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@nosana/node",
|
|
9
|
-
"version": "1.1.
|
|
9
|
+
"version": "1.1.34-rc",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@coral-xyz/anchor": "^0.28.1-beta.1",
|