dcp-worker 3.3.16 → 3.3.19

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 CHANGED
@@ -2,19 +2,16 @@
2
2
 
3
3
  This is the official DCP Worker program for the Distributive Compute Platform.
4
4
 
5
- This package implements a DCP Worker which can be executive interactively, or a system service, using
5
+ This package implements a DCP Worker which can be executed interactively, or a as system service, using
6
6
  Node.js to communicate with the scheduler and control the DCP Evaluator.
7
7
 
8
8
  A companion program, the DCP Evaluator, is required to use this worker. When you install the DCP Worker
9
9
  with your system's package manager, the installer will automatically install the DCP Evaluator as
10
- `dcp-evaluator-v8` or `dcp-evaluator`. The DCP Evaluator is a secure sandboxing tool which uses the V8 JavaScript engine
11
- and the Dawn WebGPU engine from Google to execute JavaScript, WebAssembly, and WebGPU code.
12
-
13
- You can find a complete package for Linux, including Evaluator binaries at https://archive.distributed.computer/releases/,
14
- and documentation at https://docs.distributed.computer/worker/readme.html. If you are a developer
15
- who interested in porting the Evaluator to your own platform, please contact us and we will
16
- grant you early access to the MIT-licensed source code. The build is CMake with GN and largely
17
- based around V8.
10
+ `dcp-evaluator-v8` or `dcp-evaluator`. The DCP Evaluator is a secure sandboxing tool which uses
11
+ Google's V8 JavaScript engine and Google's Dawn WebGPU implementation for secure execution of JavaScript, WebAssembly, and WebGPU code.
12
+
13
+ You can find a complete package for Linux, including Evaluator binaries at https://gitlab.com/Distributed-Compute-Protocol/dcp-native/-/releases.
14
+ The build is CMake with GN and largely based around V8.
18
15
 
19
16
  This package is also used for DCP LocalExec, a local debugging tool for developers using DCP Client.
20
17
 
@@ -0,0 +1,696 @@
1
+ #! /usr/bin/env node
2
+ /**
3
+ * @file dcp-task-probe
4
+ * Tool to discover work available on the scheduler by requesting and
5
+ * then returning slices.
6
+ *
7
+ * task request response payload currently (dec 2024) has
8
+ * - owner: scheduler's address
9
+ * - signature: ethereum signature object
10
+ * - auth:
11
+ * - workerId: our worker id
12
+ * - authSlices: job address props -> array of number
13
+ * - schedulerId: { address: scheduler's address }
14
+ * - jobCommissions: job address props
15
+ * - rate: number
16
+ * - bank account
17
+ * - body:
18
+ * - newJobs: job address props
19
+ * - address: job address
20
+ * - metrics:
21
+ * - sliceCPUTime
22
+ * - sliceGPUTime
23
+ * - sliceCPUDensity
24
+ * - sliceGPUDensity
25
+ * - lastSliceNumber
26
+ * - measuredSlices
27
+ * - alpha
28
+ * - running: true
29
+ * - requirements: object
30
+ * - public:
31
+ * - computeGroups array
32
+ * - name
33
+ * - description
34
+ * - link
35
+ * - uuid
36
+ * - codeLocation: uri
37
+ * - argumentsLocation: array
38
+ * - MROLocation: uri
39
+ * - requirePath: array
40
+ * - modulePath: array
41
+ * - dependencies: array of module identifiers
42
+ * - urlParameters:
43
+ * - job: job address
44
+ * - jobId: uuid
45
+ * - workerConsole: 0
46
+ * - worktime:
47
+ * - name
48
+ * - version
49
+ * - task: job address props -> array
50
+ * - jobAddress: address
51
+ * - uuid: uuid
52
+ * - worker: worker opaque id
53
+ * - sliceNumber
54
+ * - cotaskCount
55
+ * - isEstimationSlice: boolean
56
+ * - isLongSlice: boolean
57
+ * - timestamp: ms
58
+ * - resultStorageType: values|??
59
+ * - resultStorageDetails
60
+ * - resultStorageParams
61
+ * - computeGroupJobs: opaqueId props
62
+ * - array of jobId
63
+ * - computeGroups: opaque id -> array
64
+ * - computeGroupOrigins: {}
65
+ * - schedulerConfig:
66
+ * - shared:
67
+ * - frequencyMs
68
+ * - debuggingFactor
69
+ * - useLifetimeAnalysis
70
+ * - lifetimeFactor
71
+ * - slice
72
+ * - maxCotasking
73
+ * - multiplier
74
+ * - sigmas
75
+ * - factor
76
+ * - biasMs
77
+ * - targetTaskDuration: number in seconds
78
+ *
79
+ * @author Wes Garland, wes@distributive.network
80
+ * @date Dec 2024
81
+ */
82
+ 'use strict';
83
+
84
+ const fs = require('fs');
85
+ const path = require('path');
86
+ const debug = require('debug');
87
+ const getopt = require('posix-getopt');
88
+ const util = require('util');
89
+
90
+ function usage()
91
+ {
92
+ console.log(
93
+ `
94
+ DCP Task Probe - Copyright (c) 2024 Distributive Corp.
95
+ Released under the terms of the MIT License.
96
+
97
+ Usage: ${process.argv[0]} [option...]
98
+ Examples (bash syntax):
99
+ - # dcp-task-probe --set cores.cpu=13 --set sandbox.progressTimeoutMs=2000
100
+ - # dcp-task-probe --merge allowOrigins.fetchData=https://google.com -c
101
+ - # bin/dcp-task-probe --set minimumWage.CPU=1 --set minimumWage.GPU=3 -c
102
+ - # dcp-task-probe --merge "minimumWage=({CPU:1, GPU:3})" -c
103
+ - # dcp-task-probe --merge 'minimumWage={"CPU":1,"GPU":3}' -c
104
+ Options:
105
+ -h | --help show this help and exit
106
+ -C | --dumpConfig dump worker configuration instead of requesting a task
107
+ -v | --version increase verbosity (show job info)
108
+ -H | --hostname= set evaluator hostname (for capability probing)
109
+ -p | --port= set evaluator port number (for capability probing)
110
+ -S | --set property.name= sets a node in dcpConfig.worker to the given value
111
+ -M | --merge property.name= merge a given value to with a node in dcpConfig.worker
112
+ -a | --allowedOrigins= add url to dcpConfig.worker.allowedOrigins.any
113
+ -c | --cores= set the number of cpu,gpu cores
114
+ -u | --utilization= set the cpu,gpu utilization target
115
+ -m | --maxSandboxes= Maximum number of sandboxes
116
+ -g | --joinComputeGroup= joinKey,joinSecret or joinKey,joinHash of compute group to join
117
+ -j | --jobId= id of job to work
118
+ -r | --replay replay request from worker's last requestSnapshot
119
+ -l | --load load config from worker's last requestSnapshot
120
+ -i | --input= render the input file instead of requesting a new task
121
+ -o | --output= save the task to disk
122
+ Syntax:
123
+ The --merge and --set options have a variety of parsing options, to allow values in
124
+ dcpConfig.worker to have the correct types. The parsing method is controlled by the
125
+ first character of the value:
126
+ @ - the value is a filename; the file is read in and its contents are used in place
127
+ of the value specified on the command-line
128
+ ( - the value is parsed as a JavaScript expression
129
+ { - the value is parsed as a JSON object
130
+ # - the rest of the value is parsed as JSON
131
+ [ - the value is parsed as a JSON array
132
+ \` - the value is parsed as a JavaScript template string
133
+ ' - the value is parsed as a string
134
+ " - the value is parsed as a string
135
+ other - the value is parsed as a string, unless it can be parsed as a number
136
+ Note:
137
+ Beyond the usual dcpConfig.worker values, this program adds dcpConfig.worker.capabilities and
138
+ dcpConfig.worker.worktimes. If they are not present in the loaded config, they are automtically
139
+ populated by querying the local evaluator. If they are both specified in the config, then it
140
+ is not necessary to have a working evaluator to run this program.
141
+ Environment:
142
+ DEBUG - enables debugging, eg DEBUG='dcp-task-probe*,-dcp-task-probe:evaluator'
143
+ DCP_CONFIG - sets location of dcp-worker-config file
144
+ `);
145
+
146
+ process.exit(0);
147
+ }
148
+
149
+ function panic()
150
+ {
151
+ console.error.apply(console, arguments);
152
+ process.exit(1);
153
+ }
154
+
155
+ function detailedView(object)
156
+ {
157
+ return util.inspect(object, { depth: null, colors: process.env.FORCE_COLOR || true });
158
+ }
159
+
160
+ /**
161
+ * Determine if an string is a hash; used to differentiate between compute group join keys and hashes.
162
+ */
163
+ function isHash(b)
164
+ {
165
+ return b && b.length === 68 && b.startsWith('eh1-');
166
+ }
167
+
168
+ /**
169
+ * Evaluate a JavaScript expression within a given namespace. This function creates an IIFE and injects
170
+ * the properties of the namespace object into the IIFE's function block scope.
171
+ *
172
+ * @param {string} expression the JS expression to evaluatte
173
+ * @param {object} namespace the namespace object; each property becomes a symbol that can be used
174
+ * by the expression
175
+ */
176
+ function nsEval(expression, namespace)
177
+ {
178
+ const indirectEval = eval; // eslint-disable-line no-eval
179
+ const symbolNames = Object.keys(namespace);
180
+ const symbolValues = Object.values(namespace);
181
+ return indirectEval(`(${symbolNames}) => ${expression}`)(symbolValues);
182
+ }
183
+
184
+ /**
185
+ * Parse a value within a given namespace. See the usage function for the syntax supported by this
186
+ * function. If the value is not a string, it is not parsed and simply returned.
187
+ *
188
+ * @param {any} value the value to parse
189
+ * @param {object} namespace a namespacing object used when evaluating JS expresssions; see nsEval
190
+ * @returns {any} value or a JS value derived by parsing value
191
+ */
192
+ function parseValue(value, namespace)
193
+ {
194
+ if (typeof value !== 'string')
195
+ return;
196
+
197
+ if (value[0] === '@')
198
+ value = fs.readFileSync(value[0], 'utf-8');
199
+ switch(value[0])
200
+ {
201
+ case '#':
202
+ value = JSON.parse(value.slice(1));
203
+ break;
204
+ case '[':
205
+ case '{':
206
+ value = JSON.parse(value);
207
+ break;
208
+ case '`': /* fallthrough */
209
+ case '(':
210
+ value = nsEval(value, namespace);
211
+ break;
212
+ case "'":
213
+ if (/^'([^']*)'/.test(value))
214
+ value = value.match(/^'([^.]*)'/)[1];
215
+ break;
216
+ case '"':
217
+ if (/^"([^"]*)"/.test(value))
218
+ value = value.match(/^"([^"]*)"/)[1];
219
+ break;
220
+ default:
221
+ if (!isNaN(Number(value)))
222
+ value = Number(value);
223
+ break;
224
+ }
225
+ return value;
226
+ }
227
+
228
+ /**
229
+ * Traverse a graph by walking a dot path (eg a.b.c); ie. the path names each edge of the graph of the
230
+ * graph to traverse. The value returned is the node which is the parent of the final edge in the path,
231
+ * and the final edge the path. This is so that the calling code can replace the edge and is effectively
232
+ * a pointer to the node described by the dot path.
233
+ *
234
+ * @example:
235
+ * walkDotPath(dcpConfig.worker, 'minimumWage.CPU')
236
+ * returns [ dcpConfig.worker.minimumWage, 'CPU' ]
237
+ *
238
+ * @param {object} startNode the name of the first node in the graph
239
+ * @param {string} dotPath the names of the edges to traverse, separated by dots
240
+ * @return { Array } whose first element is the node containing parent node of the final edge and
241
+ * whose second element is the name of the final edge
242
+ */
243
+ function walkDotPath(startNode, dotPath)
244
+ {
245
+ var node = startNode;
246
+ var seen = [ 'worker' ];
247
+
248
+ dotPath = dotPath.split('.');
249
+ while(dotPath.length > 1)
250
+ {
251
+ const prop = dotPath.shift();
252
+ if (!node[prop])
253
+ panic(`invalid property ${prop} in dcpConfig.${seen.join('.')}`)
254
+ node = node[prop];
255
+ seen.push(prop);
256
+ }
257
+
258
+ return [ node, dotPath[0] ];
259
+ }
260
+
261
+ /** Parse a string of the form a,b into { cpu: a, gpu: b } */
262
+ function cpuCommaGpu(str)
263
+ {
264
+ const [cpu,gpu] = str.split(/[ ,]/);
265
+ const obj = {};
266
+ if (typeof cpu !== 'undefined' && cpu !== '')
267
+ obj.cpu = Number(cpu);
268
+ if (typeof gpu !== 'undefined')
269
+ obj.gpu = Number(gpu);
270
+ return obj;
271
+ }
272
+
273
+ /**
274
+ * Parse and process command-line options. Most options result in changes to the running dcpConfig;
275
+ * other options are reflected in the returned object:
276
+ * - dumpConfig
277
+ * - workerId
278
+ */
279
+ async function processOptions()
280
+ {
281
+ const opts = { verbose: 0 };
282
+ const parser = new getopt.BasicParser('h(help)C(dumpConfig)S:(set)M:(merge)a:(allowedOrigins)'
283
+ + 'c:(cores)u:(utilization)m:(maxSandboxes)w:(workerId)'
284
+ + 'J:(jobAddress)j:(job)g:(computeGroup)G(leaveGlobalGroup)'
285
+ + 'H:(hostname)p:(port)r(replay)l(load)i(input)o(output)'
286
+ + 'v(verbose)',
287
+ process.argv);
288
+ var opthnd;
289
+
290
+ while ((opthnd = parser.getopt()) !== undefined)
291
+ {
292
+ switch (opthnd.option)
293
+ {
294
+ case 'h':
295
+ usage();
296
+ break;
297
+
298
+ default:
299
+ throw new Error(`defined but unspecified option: -${opthnd.option}` + (opthnd.optarg ? `=${opthnd.optarg}` : ''));
300
+
301
+ case '?':
302
+ process.exit(1);
303
+ break;
304
+
305
+ case 'C':
306
+ opts.dumpConfig = true;
307
+ break;
308
+
309
+ case 'v':
310
+ opts.verbose += 1;
311
+ break;
312
+
313
+ case 'w':
314
+ opts.workerId = opthnd.optarg;
315
+ break;
316
+
317
+ case 'r':
318
+ opts.replay = true;
319
+ break;
320
+
321
+ case 'a':
322
+ dcpConfig.worker.allowedOrigins.any.push(opthnd.optarg);
323
+ break;
324
+
325
+ case 'c':
326
+ Object.assign(dcpConfig.worker.cores, cpuCommaGpu(opthnd.optarg));
327
+ break;
328
+
329
+ case 'u':
330
+ Object.assign(dcpConfig.worker.utilization, cpuCommaGpu(opthnd.optarg));
331
+ break;
332
+
333
+ case 'm':
334
+ dcpConfig.worker.maxSandboxes = Number(opthnd.optarg);
335
+ break;
336
+
337
+ case 'J':
338
+ dcpConfig.worker.jobAddresses = (dcpConfig.worker.jobAddresses || []).concat(opthnd.optarg);
339
+ break;
340
+
341
+ case 'j': /** @todo integrate with upcoming scheduler changes to opaqueId /wg dec 2024 */
342
+ dcpConfig.worker.jobs = (dcpConfig.worker.jobs || []).concat(opthnd.optarg);
343
+ break;
344
+
345
+ case 'G':
346
+ dcpConfig.worker.leavePublicGroup = true;
347
+ break;
348
+
349
+ case 'H':
350
+ dcpConfig.evaluator.location.hostname = opthnd.ortarg;
351
+ break;
352
+
353
+ case 'p':
354
+ dcpConfig.evaluator.location.port = opthnd.ortarg;
355
+ break;
356
+
357
+ case 'g':
358
+ {
359
+ const [ joinKey, joinSecret ] = opthnd.optarg.split(',');
360
+ const joinHash = joinSecret;
361
+
362
+ if (isHash(joinHash))
363
+ dcpConfig.worker.computeGroups.push({ joinKey, joinHash });
364
+ else
365
+ dcpConfig.worker.computeGroups.push({ joinKey, joinSecret });
366
+ break;
367
+ }
368
+
369
+ case 'M': /* fallthrough */
370
+ case 'S':
371
+ {
372
+ const eqIdx = opthnd.optarg.indexOf('=');
373
+ const idx = eqIdx !== -1 ? eqIdx : opthnd.optarg.length;
374
+ const value = parseValue(opthnd.optarg.slice(idx + 1), dcpConfig.worker);
375
+ const [ confNode, prop ] = walkDotPath(dcpConfig.worker, opthnd.optarg.slice(0, idx));
376
+
377
+ if (opthnd.option === 's')
378
+ confNode[prop] = value;
379
+ else
380
+ {
381
+ /* merge mode: objects are merged at the property level, except for arrays, which are concatenated */
382
+ if (confNode === dcpConfig.worker && (prop === 'worktimes' || prop === 'capabilities'))
383
+ await probeEvaluator(); /* ensure we have something to merge into */
384
+ switch(typeof confNode[prop])
385
+ {
386
+ case 'object':
387
+ {
388
+ if (!Array.isArray(confNode[prop]))
389
+ Object.assign(confNode[prop], value);
390
+ else
391
+ {
392
+ if (Array.isArray(value))
393
+ Array.push.apply(confNode, value);
394
+ else
395
+ confNode[prop].push(value);
396
+ }
397
+ break;
398
+ }
399
+ default:
400
+ confNode[prop] = value;
401
+ break;
402
+ }
403
+ }
404
+ break;
405
+ }
406
+
407
+ case 'l':
408
+ {
409
+ const wallet = require('dcp/wallet');
410
+ const snapshot = readWorkerSnapshot();
411
+ dcpConfig.worker = snapshot.workerConfig;
412
+ dcpConfig.worker.paymentAddress = new wallet.Address(dcpConfig.worker.paymentAddress);
413
+ }
414
+ }
415
+ }
416
+
417
+ return opts;
418
+ }
419
+
420
+ /**
421
+ * Probe the evaluator to determine its capabilities and supported worktimes. This function is delayed
422
+ * as late as possible during the configuration phase so that the user has the option to specify these
423
+ * completely on a system with no evaluator.
424
+ */
425
+ async function probeEvaluator()
426
+ {
427
+ if (probeEvaluator.probed)
428
+ return;
429
+ probeEvaluator.probed = true;
430
+ const probeTimer = setTimeout(() => panic('unable to probe evaluator'), 66610e3); /* Need to keep main from exiting */
431
+
432
+ if (!dcpConfig.worker.capabilities || !dcpConfig.worker.worktimes)
433
+ {
434
+ const { workerFactory } = require('dcp-client/lib/standaloneWorker');
435
+ debug('dcp-task-probe:evaluator')('Probing evaluator at', dcpConfig.evaluator.location);
436
+ const evaluatorInfo = await require('dcp/worker').probeEvaluator(workerFactory(dcpConfig.evaluator.location));
437
+ dcpConfig.worker.capabilities ||= evaluatorInfo.capabilities;
438
+ dcpConfig.worker.worktimes ||= evaluatorInfo.worktimes;
439
+ }
440
+
441
+ clearTimeout(probeTimer);
442
+ }
443
+
444
+ /**
445
+ * Returns the most recent snapshot left behind by the worker. The location of the snapshot is given by
446
+ * dcpConfig.worker.requestSnapshot. Multiple calls to this function always return the same snapshot.
447
+ * This function understands that the worker might write kvin-encoded snapshots, identitfied by their
448
+ * extension. The snapshots are written automatically by the worker during task request when the
449
+ * dcpConfig.worker.requestSnapshot is set.
450
+ */
451
+ function readWorkerSnapshot()
452
+ {
453
+ if (!dcpConfig.worker.requestSnapshot)
454
+ panic('no snapshot specified in dcpConfig.worker.requestSnapshot');
455
+
456
+ if (!readWorkerSnapshot.cache)
457
+ {
458
+ const serializer = dcpConfig.worker.requestSnapshot.endsWith('kvin') ? require('kvin').KVIN : JSON;
459
+ debug('dcp-task-probe')('loading snapshot', detailedView(dcpConfig.worker.requestSnapshot));
460
+ readWorkerSnapshot.cache = serializer.parse(fs.readFileSync(dcpConfig.worker.requestSnapshot));
461
+ }
462
+ return readWorkerSnapshot.cache;
463
+ }
464
+
465
+ /**
466
+ * Generate non-global compute group secrets, hashed with the current task distributor connection's
467
+ * dcpsid. Add in the public group if necessary.
468
+ *
469
+ * @todo unify with supervisor /wg dec2024
470
+ */
471
+ function makeComputeGroupsRequest(tdConn, dummyWorker)
472
+ {
473
+ const computeGroups = structuredClone(dcpConfig.worker.computeGroups); /* supervisor code sadly mutates this /wg dec 2024 */
474
+ const options = Object.assign({}, dcpConfig.worker, { computeGroups });
475
+ const taskDistributor = { connection: tdConn };
476
+
477
+ dummyWorker.badSupervisorBackdoor.dcp4.generateWorkerComputeGroups.apply({ taskDistributor, options });
478
+ if (!dcpConfig.worker.leavePublicGroup && !dcpConfig.worker.leaveGlobalGroup)
479
+ options.computeGroups.unshift({ joinKey: 'public' });
480
+ return options.computeGroups;
481
+ }
482
+
483
+
484
+ /**
485
+ * Request a task from the task distributor
486
+ *
487
+ * @todo unify with supervisor /wg dec 2024
488
+ * @param {Keystore} identity identity of the requesting entity
489
+ * @param {Object} opts command-line options to dcp-task-probe
490
+ * .replay truey to use request from worker snapshot instead of a generated request
491
+ * .workerId workerId for request
492
+ */
493
+ async function requestTask(identity, opts)
494
+ {
495
+ const protocol = require('dcp/protocol');
496
+ const worker = require('dcp/worker');
497
+ const tdConn = new protocol.Connection(dcpConfig.scheduler.services.taskDistributor);
498
+ const dummyWorker = new worker.Worker(identity, dcpConfig.worker); /* side effect: initializes dcpConfig.worker */
499
+ var request;
500
+
501
+ if (opts.replay)
502
+ request = readWorkerSnapshot().request;
503
+ else
504
+ request = {
505
+ supervisor: dummyWorker.supervisorVersion,
506
+ targetLoad: {
507
+ cpu: Math.min(dcpConfig.worker.maxSandboxes, dcpConfig.worker.cores.cpu * dcpConfig.worker.utilization.cpu) /* cpuCoreSpace */,
508
+ gpu: Math.min(dcpConfig.worker.maxSandboxes, dcpConfig.worker.cores.gpu * dcpConfig.worker.utilization.gpu) /* this.maxWorkingGPUs */,
509
+ longSlices: dcpConfig.worker.maxSandboxes /* Math.floor(cpuCoreSpace) */,
510
+ },
511
+ coreStats: { /** @todo why are we specifying these? /wg dec 2024 */
512
+ worker: dummyWorker.workerId,
513
+ lCores: require('os').cpus().length,
514
+ pCores: require('physical-cpu-count'),
515
+ sandbox: dcpConfig.worker.maxSandboxes,
516
+ },
517
+ jobQuanta: [ 1 ]/* this.quanta.calculateJobQuanta() */,
518
+ capabilities: dcpConfig.worker.capabilities,
519
+ paymentAddress: dcpConfig.worker.paymentAddress,
520
+ jobAddresses: dcpConfig.worker.jobAddresses || [],
521
+ workerComputeGroups: makeComputeGroupsRequest(tdConn, dummyWorker),
522
+ minimumWage: dcpConfig.worker.minimumWage,
523
+ soteriaJobs: [],
524
+ overdueSlices: [],
525
+ previouslyWorkedJobs: [], /* discrete jobs */
526
+ rejectedJobs: [],
527
+ unresolvedJobs: [],
528
+ fetchState: 1, /* normal fetch */
529
+ worktimes: dcpConfig.worker.worktimes,
530
+ };
531
+
532
+ debug('dcp-task-probe:connect')('Connecting to task distributor at', tdConn.targetDescriptor.location.href);
533
+ await tdConn.keepalive();
534
+ debug('dcp-task-probe:request')('Task Request:', request);
535
+
536
+ const rtStart = Date.now();
537
+ const response = await tdConn.request('requestTask', request);
538
+ const rtElapsed = Date.now() - rtStart;
539
+ console.log('Task Request Time: ', fmtMs(rtElapsed));
540
+
541
+ debug('dcp-task-probe:request')('Task Response:', detailedView(response));
542
+ if (!response.success)
543
+ panic('request failed:', response.payload);
544
+
545
+ return { response, workerId: request.coreStats?.worker };
546
+ }
547
+
548
+ async function returnTask(taskResponsePayload)
549
+ {
550
+ const protocol = require('dcp/protocol');
551
+ const { body, ...authorizationMessage } = taskResponsePayload;
552
+ const { auth } = taskResponsePayload;
553
+ const returnRequest = {
554
+ worker: auth.workerId,
555
+ slices: [],
556
+ };
557
+
558
+ for (const job in auth.authSlices)
559
+ {
560
+ for (const sliceNumber of auth.authSlices[job])
561
+ {
562
+ const slices = body.task[job];
563
+ returnRequest.slices.push({
564
+ sliceNumber,
565
+ job,
566
+ authorizationMessage,
567
+ isEstimationSlice: slices.filter(slice => slice.sliceNumber === sliceNumber)[0].isEstimationSlice,
568
+ status: 'return',
569
+ reason: 'task-probe',
570
+ });
571
+ }
572
+ }
573
+
574
+ debug('dcp-task-probe:request')('Return Request:', detailedView(returnRequest));
575
+ const rsConn = new protocol.Connection(dcpConfig.scheduler.services.resultSubmitter);
576
+ const response = await rsConn.request('status', returnRequest);
577
+
578
+ if (!response.success)
579
+ panic('request failed:', response.payload);
580
+
581
+ debug('dcp-task-probe:response')('Return Response:', detailedView(response));
582
+ if (!response.payload?.length)
583
+ panic('did not return task');
584
+ }
585
+
586
+ function unique(e, i, self)
587
+ {
588
+ return i === self. indexOf(e);
589
+ }
590
+
591
+ function fmtMs(ms)
592
+ {
593
+ return (ms / 1e3).toFixed(3) + 's';
594
+ }
595
+
596
+ /** Main program entry point */
597
+ async function main()
598
+ {
599
+ const wallet = require('dcp/wallet');
600
+ const identity = await wallet.getId();
601
+ const opts = await processOptions();
602
+
603
+ if (!dcpConfig.worker.paymentAddress)
604
+ dcpConfig.worker.paymentAddress = (await wallet.get()).address;
605
+
606
+ if (!opts.replay)
607
+ await probeEvaluator();
608
+
609
+ if (opts.dumpConfig)
610
+ {
611
+ /* work around the cores getter/setter that is added by the worker */
612
+ const dump = Object.assign({}, dcpConfig.worker);
613
+ dump.cores = { cpu: dcpConfig.worker.cores.cpu, gpu: dcpConfig.worker.cores.gpu };
614
+ console.log(dump);
615
+ process.exit(0);
616
+ }
617
+
618
+ const { workerId, response } = await requestTask(identity, opts);
619
+ const task = response.payload?.body?.task;
620
+ if (task)
621
+ await returnTask(response.payload);
622
+
623
+ console.log('Task Distributor: ', dcpConfig.scheduler.services.taskDistributor.location.href);
624
+ console.log('Result Submitter: ', dcpConfig.scheduler.services.resultSubmitter.location.href);
625
+ console.log('Worker Id: ', workerId);
626
+ console.log('DCP Address: ', String(identity.address));
627
+
628
+ if (!task)
629
+ {
630
+ console.error('no work');
631
+ process.exit(22);
632
+ }
633
+
634
+ const { newJobs, computeGroupJobs, computeGroupOrigins, schedulerConfig } = response.payload.body;
635
+ const cgs = {}
636
+ for (const cgId in computeGroupJobs)
637
+ cgs[cgId] = { jobCount: computeGroupJobs[cgId].length };
638
+ for (const jobId in newJobs)
639
+ {
640
+ for (const cg of newJobs[jobId].public.computeGroups)
641
+ Object.assign(cgs[cg.opaqueId], { name: cg.name, description: cg.description });
642
+ }
643
+
644
+ console.log('Total Jobs: ', Object.keys(task).length);
645
+ console.log('Total Slices: ', Object.keys(task).map(jobId => task[jobId].length).reduce((a, b) => a+b, 0));
646
+ console.log('Total CPU,GPU Time: ',
647
+ fmtMs(Object.keys(task).map(jobId => newJobs[jobId].metrics.sliceCPUTime).reduce((a, b) => a+b, 0)) + ',',
648
+ fmtMs(Object.keys(task).map(jobId => newJobs[jobId].metrics.sliceGPUTime).reduce((a, b) => a+b, 0)));
649
+ console.log('CPU,GPU Time/core: ',
650
+ fmtMs(Object.keys(task).map(jobId => newJobs[jobId].metrics.sliceCPUTime).reduce((a, b) => a+b, 0) / dcpConfig.worker.cores.cpu) + ',',
651
+ fmtMs(Object.keys(task).map(jobId => newJobs[jobId].metrics.sliceGPUTime).reduce((a, b) => a+b, 0) / dcpConfig.worker.cores.gpu));
652
+ console.log('Task Duration: ', schedulerConfig.targetTaskDuration + 's');
653
+ console.log('Compute Groups: ', Object.keys(computeGroupJobs).length);
654
+
655
+ if (opts.verbose)
656
+ {
657
+ for (const cgId in cgs)
658
+ {
659
+ console.log(' -', cgId, cgs[cgId].name);
660
+ if (computeGroupOrigins[cgId])
661
+ console.log(' allow origins:', computeGroupOrigins[cgId]);
662
+ }
663
+ }
664
+
665
+ if (opts.verbose)
666
+ {
667
+ for (const jobId of Object.keys(task))
668
+ {
669
+ console.log(`\n-- Job ${jobId} ------------------------------`);
670
+ console.log('Public Info: ', newJobs[jobId].public.name, newJobs[jobId].public.description, newJobs[jobId].public.link || '');
671
+ console.log('Compute Groups: ', newJobs[jobId].public.computeGroups.map(cg=>cg.opaqueId).filter(unique));
672
+ console.log('Slices: ', Object.keys(task[jobId]).length);
673
+ console.log('Avg Slice Time: ',
674
+ `cpu=${fmtMs(newJobs[jobId].metrics.sliceCPUTime)}`,
675
+ `gpu=${fmtMs(newJobs[jobId].metrics.sliceGPUTime)}`);
676
+ console.log('Avg Slice Density: ',
677
+ `cpu=${(newJobs[jobId].metrics.sliceCPUDensity * 100).toFixed(1)}%`,
678
+ `gpu=${(newJobs[jobId].metrics.sliceGPUDensity * 100).toFixed(1)}%`);
679
+ console.log('Worktime: ', `${newJobs[jobId].worktime.name}@${newJobs[jobId].worktime.version}`);
680
+ if (newJobs[jobId].codeLocation.startsWith('data:'))
681
+ console.log('Work Function: ', newJobs[jobId].codeLocation.length, 'bytes, inline');
682
+ else
683
+ console.log('Work Function: ', newJobs[jobId].codeLocation);
684
+
685
+ console.log('Result Storage: ', task[jobId][0].resultStorageType, task[jobId][0].resultStorageDetails);
686
+ console.log('Dependencies: ', newJobs[jobId].dependencies.map(a => path.basename(a)).join(' '));
687
+ }
688
+ }
689
+ }
690
+
691
+ /* Initialize dcp-client to use only local resources before launching the main function */
692
+ require('dcp-client').init({
693
+ progName: 'dcp-worker',
694
+ parseArgv: false,
695
+ configName: process.env.DCP_CONFIG || '../etc/dcp-worker-config', /* => .js or .json */
696
+ }).then(main);
package/bin/dcp-worker CHANGED
@@ -130,14 +130,7 @@ function parseCliArgs()
130
130
  alias: 'p',
131
131
  describe: 'Evaluator port',
132
132
  type: 'number',
133
- default: Number(dcpConfig.evaluator.listen.port),
134
- },
135
- priorityOnly: {
136
- alias: 'P',
137
- hidden: true,
138
- describe: 'Set the priority mode [deprecated]',
139
- type: 'boolean',
140
- default: false
133
+ default: Number(dcpConfig.evaluator.location.port),
141
134
  },
142
135
  'job-id': {
143
136
  alias: 'j',
@@ -403,7 +396,6 @@ z */
403
396
  if (dcpWorkerOptions.jobAddresses === false || dcpWorkerOptions.jobAddresses === undefined)
404
397
  dcpWorkerOptions.jobAddresses = [];
405
398
  dcpWorkerOptions.jobAddresses.push(...cliArgs.jobId);
406
- dcpWorkerOptions.priorityOnly = true;
407
399
  }
408
400
 
409
401
  if (cliArgs.allowedOrigins)
@@ -462,10 +454,12 @@ z */
462
454
  else
463
455
  {
464
456
  const errorCode = error.code ?? error.errorCode;
457
+ const location = error.stack.split('\n')[1].replace(/^\s*/,'');
458
+ let message = error.message;
459
+
465
460
  if (errorCode)
466
- console.error(`Error: ${error.message}: error.code ${errorCode}`);
467
- else
468
- console.error(`Error: ${error.message}`);
461
+ message += ` (${error.code})`;
462
+ console.error(`Error: ${error.message} ${location}`);
469
463
  }
470
464
  });
471
465
  require('../lib/default-ui-events').hook(nascentWorker, cliArgs);
@@ -533,29 +527,26 @@ z */
533
527
  if (telnetd.hasOwnProperty('port'))
534
528
  bannerLog(` ! telnetd listening on port ${telnetd.port}`);
535
529
 
536
- const { worktimes } = require('dcp-client/libexec/sandbox/worktimes');
537
- if (Object.keys(worktimes).length > 0)
538
- {
539
- bannerLog(' . Worktimes Available:');
540
- for (const wt of worktimes)
541
- bannerLog(` -\t${wt.name}@${wt.versions.join(';')}`);
542
- }
543
-
544
- let webgpuInfo;
530
+ let workerInfo;
545
531
  console.throb('log', ' ! Waiting for dcp-evaluator to start...');
546
- for (let i=0; !webgpuInfo; i++)
532
+ for (let i=0; !workerInfo; i++)
547
533
  {
548
- webgpuInfo = await require('../lib/webgpu-info').checkWebGPU(sawOptions);
549
- if (webgpuInfo)
534
+ workerInfo = await require('../lib/worker-info').getEvaluatorInformation(sawOptions);
535
+ if (workerInfo)
550
536
  break;
551
537
  console.throb();
552
538
  await a$sleep(Math.max(i + 1, 10) / 4);
553
539
  console.throb();
554
540
  }
555
541
 
556
- if (!webgpuInfo)
557
- bannerLog(' . WebGPU detection failed');
558
- else if (!webgpuInfo.enabled)
542
+ if (Object.keys(workerInfo.worktimes).length > 0)
543
+ {
544
+ bannerLog(' . Worktimes Available:');
545
+ for (const wt of workerInfo.worktimes)
546
+ bannerLog(` -\t${wt.name}@${wt.versions.join(';')}`);
547
+ }
548
+
549
+ if (!workerInfo.webgpu.enabled)
559
550
  {
560
551
  bannerLog(' . WebGPU not enabled');
561
552
  systemStateInfo.gpu = false;
@@ -563,13 +554,13 @@ z */
563
554
  else
564
555
  {
565
556
  bannerLog(' . WebGPU available. GPU info:');
566
- for (let descriptor in webgpuInfo.info)
567
- bannerLog(` -\t${descriptor}: ${webgpuInfo.info[descriptor]}`);
568
- systemStateInfo.gpu = Object.assign({}, webgpuInfo.info);
557
+ for (let descriptor in workerInfo.webgpu.info)
558
+ bannerLog(` -\t${descriptor}: ${workerInfo.webgpu.info[descriptor]}`);
559
+ systemStateInfo.gpu = Object.assign({}, workerInfo.webgpu.info);
569
560
  systemStateInfo.gpu.device
570
561
  for (let descriptor in bannedGPUs)
571
562
  {
572
- if (bannedGPUs[descriptor].test(webgpuInfo.info[descriptor]))
563
+ if (bannedGPUs[descriptor].test(workerInfo.webgpu.info[descriptor]))
573
564
  {
574
565
  console.error(' * This GPU is not supported; disabling');
575
566
  dcpWorkerOptions.cores = { cpu: dcpWorkerOptions.cores.cpu, gpu: 0 };
@@ -834,6 +825,7 @@ function handleSigDeath(signalName, signal)
834
825
  if (handleSigDeath.count === 3)
835
826
  die();
836
827
  }
828
+ globalThis.die = () => handleSigDeath('QUIT', 15);
837
829
 
838
830
  /**
839
831
  * Returns the duration of the cleanup timeout in milliseconds. It is possible to specify zero.
@@ -44,11 +44,12 @@ exports.init = function dashboard$$init(worker, options, bannerLogCumulative)
44
44
  var screen = blessed.screen(screenConf);
45
45
 
46
46
  worker.on('end', () => {
47
- screen.destroy();
48
- screen = false;
47
+ if (screen?.destroy)
48
+ screen.destroy();
49
+ screen.destroy = false;
49
50
  });
50
51
  process.on('exit', () => {
51
- if (screen)
52
+ if (screen?.destroy)
52
53
  screen.destroy();
53
54
  });
54
55
 
@@ -1,7 +1,9 @@
1
1
  /**
2
- * @file webgpu-capabilities.js
3
- * Check evaluator to determine if webgpu is enabled, and what capabilities it has.
4
- *
2
+ * @file worker-reporting.js
3
+ * Check evaluator to determine if certain information including:
4
+ * - if webgpu is enabled, and what capabilities it has.
5
+ * - supported worktimes for the evaluator
6
+ *
5
7
  * @author Ryan Saweczko, ryansaweczko@distributive.network
6
8
  *
7
9
  * @date Sept 2024
@@ -9,19 +11,20 @@
9
11
  'use strict';
10
12
 
11
13
  const kvin = require('kvin');
12
- const evaluatorId = 'webgpuCheck';
14
+ const evaluatorId = 'reportingCheck';
13
15
 
14
- async function eval$$webgpu()
16
+ async function eval$$getInfo()
15
17
  {
18
+ const info = {};
16
19
  if (typeof initWebGPU === 'function')
17
20
  await initWebGPU();
18
21
 
19
22
  if (!(typeof globalThis.navigator?.gpu === 'object'))
20
- return { enabled: false };
23
+ info.webgpu = { enabled: false };
21
24
 
22
25
  try
23
26
  {
24
- const info = {};
27
+ const webgpuInfo = {};
25
28
  const adapter = await navigator.gpu.requestAdapter();
26
29
  /*
27
30
  * Newer versions of dcp-evaluator use different methods to access the adapter info as the api changes/bugs are fixed.
@@ -35,29 +38,34 @@ async function eval$$webgpu()
35
38
  if (adapter.info)
36
39
  {
37
40
  for (let key in adapter.info)
38
- info[key] = adapter.info[key];
39
- return { enabled: true, info: info };
41
+ webgpuInfo[key] = adapter.info[key];
42
+ info.webgpu = { enabled: true, info: webgpuInfo };
43
+ }
44
+ else
45
+ {
46
+ const properties = ['vendor', 'architecture', 'device', 'description'];
47
+ const adapterInfo = await adapter.requestAdapterInfo();
48
+ for (let key of properties)
49
+ webgpuInfo[key] = adapterInfo[key];
50
+ info.webgpu = { enabled: true, info: webgpuInfo };
40
51
  }
41
- const properties = ['vendor', 'architecture', 'device', 'description'];
42
- const adapterInfo = await adapter.requestAdapterInfo();
43
- for (let key of properties)
44
- info[key] = adapterInfo[key];
45
- return { enabled: true, info: info };
46
52
  }
47
53
  catch (err)
48
54
  {
49
- return { enabled: false };
55
+ info.webgpu = { enabled: false };
50
56
  }
57
+
58
+ info.worktimes = globalThis.worktimes;
59
+
60
+ return info;
51
61
  }
52
62
 
53
63
  /**
54
- * Connect to an evaluator instance and return back if webGPU is enabled, and if it is some information on the
55
- * GPU device.
64
+ * Connect to an evaluator instance and return back information on the evaluator
56
65
  * @param {object} evaluatorConfig - Object containing the hostname and port to connect to the evaluator instance on
57
- * @returns {Promise} which resolves with undefined detection failed (ie couldn't connect to evaluator),
58
- * or the object {enabled, info} with info on if webgpu exists, and if so what the gpu is.
66
+ * @returns {Promise} which resolves with undefined detection failed (ie couldn't connect to evaluator), or the information object
59
67
  */
60
- function checkEvaluatorWebGPU(evaluatorConfig)
68
+ function getEvaluatorInformation(evaluatorConfig)
61
69
  {
62
70
  const StandaloneWorker = require('dcp-client/lib/standaloneWorker').workerFactory(evaluatorConfig);
63
71
  const { a$sleep } = require('dcp/utils');
@@ -65,7 +73,7 @@ function checkEvaluatorWebGPU(evaluatorConfig)
65
73
  const evaluatorHandle = new StandaloneWorker({ name: 'DCP Sandbox #1', });
66
74
 
67
75
  var noResponse, resolve;
68
- const p$webgpuInfo = new Promise((res, rej) => { resolve = res; }).finally(() => {
76
+ const p$workerInfo = new Promise((res, rej) => { resolve = res; }).finally(() => {
69
77
  evaluatorHandle.terminate();
70
78
  noResponse.intr();
71
79
  });
@@ -84,11 +92,11 @@ function checkEvaluatorWebGPU(evaluatorConfig)
84
92
 
85
93
  const message = {
86
94
  request: 'eval',
87
- data: '(' + eval$$webgpu.toString() + ')()',
95
+ data: '(' + eval$$getInfo.toString() + ')()',
88
96
  msgId: evaluatorId,
89
97
  }
90
98
  evaluatorHandle.postMessage(kvin.marshal(message));
91
- return p$webgpuInfo;
99
+ return p$workerInfo;
92
100
  }
93
101
 
94
- exports.checkWebGPU = checkEvaluatorWebGPU;
102
+ exports.getEvaluatorInformation = getEvaluatorInformation;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dcp-worker",
3
- "version": "3.3.16",
3
+ "version": "3.3.19",
4
4
  "description": "Node.js Worker for Distributive Compute Platform",
5
5
  "main": "bin/dcp-worker",
6
6
  "keywords": [
@@ -40,7 +40,7 @@
40
40
  "blessed": "^0.1.81",
41
41
  "blessed-contrib": "4.11.0",
42
42
  "chalk": "^4.1.0",
43
- "dcp-client": "^4.4.21",
43
+ "dcp-client": "^4.4.24",
44
44
  "kvin": "^1.2.7",
45
45
  "posix-getopt": "^1.2.1",
46
46
  "semver": "^7.3.8",