claude-flow 3.61.1 → 3.62.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/package.json +1 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +3 -3
- package/v3/@claude-flow/cli/dist/src/mcp-tools/rure-tools.js +59 -0
- package/v3/@claude-flow/cli/dist/src/rure/cuda-cubin.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/rure/cuda-cubin.js +17 -2
- package/v3/@claude-flow/cli/dist/src/rure/cuda-occupancy.d.ts +59 -0
- package/v3/@claude-flow/cli/dist/src/rure/cuda-occupancy.js +69 -0
- package/v3/@claude-flow/cli/dist/src/rure/cuda-plan.d.ts +84 -0
- package/v3/@claude-flow/cli/dist/src/rure/cuda-plan.js +197 -0
- package/v3/@claude-flow/cli/dist/src/services/headless-worker-executor.js +56 -19
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +1 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +11 -2
- package/v3/@claude-flow/cli/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generation": 7,
|
|
4
|
-
"generatedAt": "2026-10-
|
|
5
|
-
"gitSha": "
|
|
4
|
+
"generatedAt": "2026-10-24T23:47:19.593Z",
|
|
5
|
+
"gitSha": "973d0ab4",
|
|
6
6
|
"catalog": {
|
|
7
7
|
"agents": 177,
|
|
8
|
-
"tools":
|
|
8
|
+
"tools": 440,
|
|
9
9
|
"skills": 34
|
|
10
10
|
},
|
|
11
11
|
"benchmark": null
|
|
@@ -16,6 +16,7 @@ import { parseBinary, extractStrings, MAX_BINARY_BYTES } from '../rure/binary-pa
|
|
|
16
16
|
import { parseDotNet } from '../rure/dotnet-parse.js';
|
|
17
17
|
import { analyzeEvm, parseHex, MAX_EVM_BYTES } from '../rure/evm-parse.js';
|
|
18
18
|
import { analyzeCuda } from '../rure/cuda-parse.js';
|
|
19
|
+
import { planCuda, MAX_BLOCK_SIZES, MAX_DYNAMIC_SHARED_BYTES } from '../rure/cuda-plan.js';
|
|
19
20
|
function fail(error) {
|
|
20
21
|
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error }) }], isError: true };
|
|
21
22
|
}
|
|
@@ -408,5 +409,63 @@ export const rureTools = [
|
|
|
408
409
|
}
|
|
409
410
|
},
|
|
410
411
|
},
|
|
412
|
+
{
|
|
413
|
+
name: 'rure_cuda_launch_plan',
|
|
414
|
+
description: 'Turn the static CUDA inventory of a program into a plan for measuring it (ADR-501): for each kernel of each architecture, its theoretical occupancy at a set of block sizes (blocks and warps resident per multiprocessor, and which resource sets the limit: registers, shared memory, warps or block slots), the block sizes that reach the most, and flags (local memory, register- or shared-memory-limited). The occupancy model reproduces NVIDIA\'s cuda_occupancy.h on every architecture it tabulates (sm_70, 75, 80, 86, 87, 89, 90, 120); any other architecture gets no figure. With includeHarness it also returns the text of a small CUDA driver-API program you can build to check the numbers on your own GPU and, with arguments you supply, to time launches. Nothing is compiled, loaded or run by this tool, and no text from the analyzed file reaches the harness. Occupancy bounds performance and does not predict it. Names from the file are untrusted and screened by AIDefence. Use when you have a CUDA binary, cubin or fatbin and need to decide which kernels and block sizes are worth benchmarking before running anything: rure_analyze_cuda alone lists registers and memory but not what they allow on a given GPU.',
|
|
415
|
+
category: 'rure',
|
|
416
|
+
tags: ['reverse-engineering', 'cuda', 'gpu', 'occupancy', 'benchmark-plan', 'static-analysis'],
|
|
417
|
+
inputSchema: {
|
|
418
|
+
type: 'object',
|
|
419
|
+
properties: {
|
|
420
|
+
path: { type: 'string', description: 'Absolute path to a CUDA host binary (ELF/PE), a .fatbin or a .cubin file' },
|
|
421
|
+
arch: { type: 'string', description: 'Plan only this architecture, for example sm_86 (default: every architecture with compiled code in the file)' },
|
|
422
|
+
blockSizes: { type: 'array', items: { type: 'integer', minimum: 1, maximum: 1024 }, maxItems: MAX_BLOCK_SIZES, description: 'Block sizes to evaluate (default 32, 64, 96, 128, 192, 256, 384, 512, 768, 1024)' },
|
|
423
|
+
dynamicSharedBytes: { type: 'integer', minimum: 0, maximum: MAX_DYNAMIC_SHARED_BYTES, description: 'Dynamic shared memory per block the launch will use; it is not in the file (default 0)' },
|
|
424
|
+
includeHarness: { type: 'boolean', description: 'Also return the text of the occupancy-check / benchmark program (default false)' },
|
|
425
|
+
},
|
|
426
|
+
required: ['path'],
|
|
427
|
+
},
|
|
428
|
+
handler: async (input) => {
|
|
429
|
+
const v = validatePath(input.path, 'path');
|
|
430
|
+
if (!v.valid)
|
|
431
|
+
return fail(v.error);
|
|
432
|
+
if (input.arch !== undefined && (typeof input.arch !== 'string' || !/^sm_\d{2,3}$/.test(input.arch)))
|
|
433
|
+
return fail('arch must look like sm_86');
|
|
434
|
+
if (input.blockSizes !== undefined && !Array.isArray(input.blockSizes))
|
|
435
|
+
return fail('blockSizes must be an array of integers');
|
|
436
|
+
if (input.dynamicSharedBytes !== undefined && typeof input.dynamicSharedBytes !== 'number')
|
|
437
|
+
return fail('dynamicSharedBytes must be an integer');
|
|
438
|
+
try {
|
|
439
|
+
const r = readCapped(v.sanitized, MAX_BINARY_BYTES);
|
|
440
|
+
if (!r.ok)
|
|
441
|
+
return fail(r.error);
|
|
442
|
+
const info = analyzeCuda(r.bytes, { includePtx: false });
|
|
443
|
+
const sc = new Screener();
|
|
444
|
+
const names = await sc.screen(info.kernels.map((k) => k.name));
|
|
445
|
+
const idents = await sc.screen(info.kernels.map((k) => k.identifier ?? ''));
|
|
446
|
+
const screened = { ...info, kernels: info.kernels.map((k, i) => ({ ...k, name: names[i], identifier: k.identifier === null ? null : idents[i] })) };
|
|
447
|
+
const plan = planCuda(screened, {
|
|
448
|
+
...(input.arch !== undefined && { arch: input.arch }),
|
|
449
|
+
...(input.blockSizes !== undefined && { blockSizes: input.blockSizes }),
|
|
450
|
+
...(input.dynamicSharedBytes !== undefined && { dynamicSharedBytes: input.dynamicSharedBytes }),
|
|
451
|
+
includeHarness: input.includeHarness === true,
|
|
452
|
+
});
|
|
453
|
+
const kernels = plan.archs.reduce((n, a) => n + a.kernels.length, 0);
|
|
454
|
+
return ok({
|
|
455
|
+
source: info.source,
|
|
456
|
+
...plan,
|
|
457
|
+
summary: { archs: plan.archs.length, kernels },
|
|
458
|
+
warnings: info.warnings,
|
|
459
|
+
redactedCount: sc.redacted,
|
|
460
|
+
screened: sc.screened,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
if (err instanceof RangeError)
|
|
465
|
+
return fail(err.message);
|
|
466
|
+
return fail(`cuda launch plan failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
},
|
|
411
470
|
];
|
|
412
471
|
//# sourceMappingURL=rure-tools.js.map
|
|
@@ -29,6 +29,13 @@ export interface CubinInfo {
|
|
|
29
29
|
warnings: string[];
|
|
30
30
|
truncated: boolean;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* The SM number in `e_flags`. Where it sits depends on the ABI the toolchain wrote: a cubin from CUDA 12 (ELF OS ABI 51) for sm_70 to sm_90 keeps it in the
|
|
34
|
+
* low byte (sm_70 is 0x460546: 0x46 twice, around the texture and address bits), while CUDA 13, and CUDA 12.8 for sm_120, use OS ABI 65 and the second byte
|
|
35
|
+
* (sm_80 is 0x06005004). Reading the second byte of an ABI 51 cubin gave "sm_5" for every one of them. An OS ABI we have not seen falls back to whichever
|
|
36
|
+
* byte is a plausible SM number.
|
|
37
|
+
*/
|
|
38
|
+
export declare function smOfFlags(flags: number, osAbi: number): number | null;
|
|
32
39
|
export declare const isCubin: (b: Buffer) => boolean;
|
|
33
40
|
/** `_Z5saxpyifPKfPf` -> `saxpy`; anything with a namespace, template or other prefix -> null. */
|
|
34
41
|
export declare function simpleIdentifier(name: string): string | null;
|
|
@@ -11,6 +11,22 @@ const NAME_MAX = 200;
|
|
|
11
11
|
const STO_CUDA_ENTRY = 0x10;
|
|
12
12
|
const EIATTR_REGCOUNT = 0x2f;
|
|
13
13
|
const MAX_INFO_RECORDS = 4096;
|
|
14
|
+
/**
|
|
15
|
+
* The SM number in `e_flags`. Where it sits depends on the ABI the toolchain wrote: a cubin from CUDA 12 (ELF OS ABI 51) for sm_70 to sm_90 keeps it in the
|
|
16
|
+
* low byte (sm_70 is 0x460546: 0x46 twice, around the texture and address bits), while CUDA 13, and CUDA 12.8 for sm_120, use OS ABI 65 and the second byte
|
|
17
|
+
* (sm_80 is 0x06005004). Reading the second byte of an ABI 51 cubin gave "sm_5" for every one of them. An OS ABI we have not seen falls back to whichever
|
|
18
|
+
* byte is a plausible SM number.
|
|
19
|
+
*/
|
|
20
|
+
export function smOfFlags(flags, osAbi) {
|
|
21
|
+
const low = flags & 0xff;
|
|
22
|
+
const second = (flags >>> 8) & 0xff;
|
|
23
|
+
const plausible = (n) => n >= 20 && n <= 199;
|
|
24
|
+
if (osAbi === 65)
|
|
25
|
+
return second;
|
|
26
|
+
if (osAbi === 51)
|
|
27
|
+
return low;
|
|
28
|
+
return plausible(second) ? second : plausible(low) ? low : null;
|
|
29
|
+
}
|
|
14
30
|
export const isCubin = (b) => b.length >= 0x40 && b[0] === 0x7f && b.toString('latin1', 1, 4) === 'ELF' && b[4] === 2 && b[5] === 1 && b.readUInt16LE(0x12) === EM_CUDA;
|
|
15
31
|
const cstr = (b, at, end) => {
|
|
16
32
|
if (at < 0 || at >= end)
|
|
@@ -59,8 +75,7 @@ export function parseCubin(b) {
|
|
|
59
75
|
return info;
|
|
60
76
|
}
|
|
61
77
|
try {
|
|
62
|
-
|
|
63
|
-
info.archFlag = (flags >>> 8) & 0xff;
|
|
78
|
+
info.archFlag = smOfFlags(b.readUInt32LE(0x30), b[7]);
|
|
64
79
|
const shoff = Number(b.readBigUInt64LE(0x28));
|
|
65
80
|
const entsize = b.readUInt16LE(0x3a);
|
|
66
81
|
let count = b.readUInt16LE(0x3c);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ruRE CUDA launch plan, part 1 (ADR-501): theoretical occupancy from what the static reader found. Given an architecture's limits and a
|
|
3
|
+
* kernel's registers, static shared memory and a block size, how many blocks fit on one multiprocessor and which resource stops the next one.
|
|
4
|
+
* Pure arithmetic over a table: no device, no driver, nothing is run. The model is the one CUDA's occupancy calculator uses (warp, block,
|
|
5
|
+
* register and shared-memory limits, each with its allocation granularity); it was written here from that model and is checked, over the whole
|
|
6
|
+
* grid of resource combinations, against NVIDIA's own `cuda_occupancy.h` (rure-cuda-occupancy-oracle.test.ts) and on real devices.
|
|
7
|
+
* Theoretical occupancy bounds how many warps can be resident. It is not a performance number.
|
|
8
|
+
*/
|
|
9
|
+
export type Limiter = 'warps' | 'registers' | 'sharedMemory' | 'blocks' | 'barriers';
|
|
10
|
+
/** Where a table row's numbers come from: read off a device by the CUDA runtime, or taken from NVIDIA's documentation. */
|
|
11
|
+
export type Provenance = 'device-checked' | 'documentation';
|
|
12
|
+
export interface ArchLimits {
|
|
13
|
+
/** `sm_NN`, as the fatbin names it. */
|
|
14
|
+
arch: string;
|
|
15
|
+
major: number;
|
|
16
|
+
minor: number;
|
|
17
|
+
maxThreadsPerSM: number;
|
|
18
|
+
maxBlocksPerSM: number;
|
|
19
|
+
maxThreadsPerBlock: number;
|
|
20
|
+
regsPerSM: number;
|
|
21
|
+
regsPerBlock: number;
|
|
22
|
+
maxRegsPerThread: number;
|
|
23
|
+
sharedPerSM: number;
|
|
24
|
+
sharedPerBlock: number;
|
|
25
|
+
sharedPerBlockOptin: number;
|
|
26
|
+
/** Shared memory the driver reserves in every block (counted against the block's allocation). */
|
|
27
|
+
sharedReservedPerBlock: number;
|
|
28
|
+
/**
|
|
29
|
+
* Whether a kernel's `.nv.shared.<kernel>` section already includes that reservation. From sm_90 it does (the compiler adds it, the driver reports
|
|
30
|
+
* the size without it, and the occupancy calculation adds it back), so the figure to use is the section size less the reservation.
|
|
31
|
+
*/
|
|
32
|
+
cubinSharedIncludesReserve: boolean;
|
|
33
|
+
/** The shared-memory sizes an SM can be configured to, ascending; the SM's allocation rounds up to one of them. */
|
|
34
|
+
carveouts: readonly number[];
|
|
35
|
+
provenance: Provenance;
|
|
36
|
+
}
|
|
37
|
+
/** Architectures with a table. Anything else gets no occupancy figure rather than a guess. */
|
|
38
|
+
export declare const ARCH_LIMITS: Readonly<Record<string, ArchLimits>>;
|
|
39
|
+
export declare const limitsFor: (arch: string | null) => ArchLimits | null;
|
|
40
|
+
export interface Kernel {
|
|
41
|
+
registers: number;
|
|
42
|
+
staticSharedBytes: number;
|
|
43
|
+
dynamicSharedBytes: number;
|
|
44
|
+
blockSize: number;
|
|
45
|
+
}
|
|
46
|
+
export interface Occupancy {
|
|
47
|
+
/** Blocks resident on one multiprocessor; 0 means the block cannot be launched at all. */
|
|
48
|
+
blocks: number;
|
|
49
|
+
/** Every resource that sets that number (ties are all listed). */
|
|
50
|
+
limiters: Limiter[];
|
|
51
|
+
/** What each resource alone would allow. */
|
|
52
|
+
limits: Record<Limiter, number>;
|
|
53
|
+
warpsPerBlock: number;
|
|
54
|
+
activeWarps: number;
|
|
55
|
+
/** activeWarps over the multiprocessor's warp capacity, 0 to 1. */
|
|
56
|
+
occupancy: number;
|
|
57
|
+
}
|
|
58
|
+
export declare function occupancy(l: ArchLimits, k: Kernel): Occupancy;
|
|
59
|
+
//# sourceMappingURL=cuda-occupancy.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ruRE CUDA launch plan, part 1 (ADR-501): theoretical occupancy from what the static reader found. Given an architecture's limits and a
|
|
3
|
+
* kernel's registers, static shared memory and a block size, how many blocks fit on one multiprocessor and which resource stops the next one.
|
|
4
|
+
* Pure arithmetic over a table: no device, no driver, nothing is run. The model is the one CUDA's occupancy calculator uses (warp, block,
|
|
5
|
+
* register and shared-memory limits, each with its allocation granularity); it was written here from that model and is checked, over the whole
|
|
6
|
+
* grid of resource combinations, against NVIDIA's own `cuda_occupancy.h` (rure-cuda-occupancy-oracle.test.ts) and on real devices.
|
|
7
|
+
* Theoretical occupancy bounds how many warps can be resident. It is not a performance number.
|
|
8
|
+
*/
|
|
9
|
+
const K = 1024;
|
|
10
|
+
const WARP = 32;
|
|
11
|
+
const REG_UNIT = 256;
|
|
12
|
+
const SUB_PARTITIONS = 4;
|
|
13
|
+
const base = { maxThreadsPerBlock: 1024, regsPerSM: 65536, regsPerBlock: 65536, maxRegsPerThread: 256, sharedPerBlock: 48 * K };
|
|
14
|
+
const row = (arch, major, minor, o) => ({ arch, major, minor, ...base, ...o });
|
|
15
|
+
/** Architectures with a table. Anything else gets no occupancy figure rather than a guess. */
|
|
16
|
+
export const ARCH_LIMITS = Object.freeze({
|
|
17
|
+
sm_70: row('sm_70', 7, 0, { maxThreadsPerSM: 2048, maxBlocksPerSM: 32, sharedPerSM: 96 * K, sharedPerBlockOptin: 96 * K, sharedReservedPerBlock: 0, cubinSharedIncludesReserve: false, carveouts: [0, 8 * K, 16 * K, 32 * K, 64 * K, 96 * K], provenance: 'documentation' }),
|
|
18
|
+
sm_75: row('sm_75', 7, 5, { maxThreadsPerSM: 1024, maxBlocksPerSM: 16, sharedPerSM: 64 * K, sharedPerBlockOptin: 64 * K, sharedReservedPerBlock: 0, cubinSharedIncludesReserve: false, carveouts: [32 * K, 64 * K], provenance: 'documentation' }),
|
|
19
|
+
sm_80: row('sm_80', 8, 0, { maxThreadsPerSM: 2048, maxBlocksPerSM: 32, sharedPerSM: 164 * K, sharedPerBlockOptin: 163 * K, sharedReservedPerBlock: K, cubinSharedIncludesReserve: false, carveouts: [0, 8 * K, 16 * K, 32 * K, 64 * K, 100 * K, 132 * K, 164 * K], provenance: 'device-checked' }),
|
|
20
|
+
sm_86: row('sm_86', 8, 6, { maxThreadsPerSM: 1536, maxBlocksPerSM: 16, sharedPerSM: 100 * K, sharedPerBlockOptin: 99 * K, sharedReservedPerBlock: K, cubinSharedIncludesReserve: false, carveouts: [0, 8 * K, 16 * K, 32 * K, 64 * K, 100 * K], provenance: 'device-checked' }),
|
|
21
|
+
sm_87: row('sm_87', 8, 7, { maxThreadsPerSM: 1536, maxBlocksPerSM: 16, sharedPerSM: 164 * K, sharedPerBlockOptin: 163 * K, sharedReservedPerBlock: K, cubinSharedIncludesReserve: false, carveouts: [0, 8 * K, 16 * K, 32 * K, 64 * K, 100 * K, 132 * K, 164 * K], provenance: 'documentation' }),
|
|
22
|
+
sm_89: row('sm_89', 8, 9, { maxThreadsPerSM: 1536, maxBlocksPerSM: 24, sharedPerSM: 100 * K, sharedPerBlockOptin: 99 * K, sharedReservedPerBlock: K, cubinSharedIncludesReserve: false, carveouts: [0, 8 * K, 16 * K, 32 * K, 64 * K, 100 * K], provenance: 'device-checked' }),
|
|
23
|
+
sm_90: row('sm_90', 9, 0, { maxThreadsPerSM: 2048, maxBlocksPerSM: 32, sharedPerSM: 228 * K, sharedPerBlockOptin: 227 * K, sharedReservedPerBlock: K, cubinSharedIncludesReserve: true, carveouts: [0, 8 * K, 16 * K, 32 * K, 64 * K, 100 * K, 132 * K, 164 * K, 196 * K, 228 * K], provenance: 'device-checked' }),
|
|
24
|
+
sm_120: row('sm_120', 12, 0, { maxThreadsPerSM: 1536, maxBlocksPerSM: 24, sharedPerSM: 100 * K, sharedPerBlockOptin: 99 * K, sharedReservedPerBlock: K, cubinSharedIncludesReserve: true, carveouts: [0, 8 * K, 16 * K, 32 * K, 64 * K, 100 * K], provenance: 'device-checked' }),
|
|
25
|
+
});
|
|
26
|
+
export const limitsFor = (arch) => (arch !== null && Object.hasOwn(ARCH_LIMITS, arch) ? ARCH_LIMITS[arch] : null);
|
|
27
|
+
const up = (n, unit) => Math.ceil(n / unit) * unit;
|
|
28
|
+
const FOREVER = Number.MAX_SAFE_INTEGER;
|
|
29
|
+
const whole = (v, name, min) => {
|
|
30
|
+
if (!Number.isInteger(v) || v < min || v > 2 ** 31)
|
|
31
|
+
throw new RangeError(`${name} must be an integer from ${min}`);
|
|
32
|
+
};
|
|
33
|
+
/** The block's shared-memory allocation, rounded up to the smallest configuration the multiprocessor offers; null when none is large enough. */
|
|
34
|
+
const carveoutFor = (l, bytes) => l.carveouts.find((size) => bytes <= size && (bytes > 0 || size === 0 || l.major === 7)) ?? null;
|
|
35
|
+
export function occupancy(l, k) {
|
|
36
|
+
whole(k.registers, 'registers', 0);
|
|
37
|
+
whole(k.staticSharedBytes, 'staticSharedBytes', 0);
|
|
38
|
+
whole(k.dynamicSharedBytes, 'dynamicSharedBytes', 0);
|
|
39
|
+
whole(k.blockSize, 'blockSize', 1);
|
|
40
|
+
const warps = Math.ceil(k.blockSize / WARP);
|
|
41
|
+
const warpCapacity = l.maxThreadsPerSM / WARP;
|
|
42
|
+
const byWarps = k.blockSize > l.maxThreadsPerBlock ? 0 : Math.floor(warpCapacity / warps);
|
|
43
|
+
const byBlocks = l.maxBlocksPerSM;
|
|
44
|
+
const perWarp = up(k.registers * WARP, REG_UNIT);
|
|
45
|
+
const perBlock = perWarp * warps;
|
|
46
|
+
const assumed = perWarp * up(warps, SUB_PARTITIONS);
|
|
47
|
+
let byRegisters = FOREVER;
|
|
48
|
+
if (l.regsPerBlock < assumed || l.regsPerBlock < perBlock || k.registers > l.maxRegsPerThread)
|
|
49
|
+
byRegisters = 0;
|
|
50
|
+
else if (perWarp > 0)
|
|
51
|
+
byRegisters = Math.floor((Math.floor(l.regsPerSM / SUB_PARTITIONS / perWarp) * SUB_PARTITIONS) / warps);
|
|
52
|
+
const granule = l.major >= 8 ? 128 : 256;
|
|
53
|
+
const allocated = up(k.staticSharedBytes + l.sharedReservedPerBlock + k.dynamicSharedBytes, granule);
|
|
54
|
+
const blockLimit = l.sharedPerBlock + (l.major >= 8 ? l.sharedReservedPerBlock : 0);
|
|
55
|
+
let bySharedMemory = 0;
|
|
56
|
+
if (allocated <= blockLimit) {
|
|
57
|
+
const preferred = carveoutFor(l, l.sharedPerSM);
|
|
58
|
+
const fits = preferred !== null && preferred >= allocated ? preferred : l.major >= 7 ? carveoutFor(l, allocated) : l.sharedPerSM;
|
|
59
|
+
bySharedMemory = fits === null ? 0 : allocated > 0 ? Math.floor(fits / allocated) : FOREVER;
|
|
60
|
+
}
|
|
61
|
+
// From compute capability 9 each resident block holds a hardware barrier; a kernel using one barrier (the default) is held to the slots there are.
|
|
62
|
+
// On sm_90 there are two per block slot, so this never binds; on sm_120 there is one, so it ties with the block limit and NVIDIA reports both.
|
|
63
|
+
const byBarriers = l.major < 9 ? FOREVER : l.major === 9 ? byBlocks * 2 : byBlocks;
|
|
64
|
+
const limits = { warps: byWarps, registers: byRegisters, sharedMemory: bySharedMemory, blocks: byBlocks, barriers: byBarriers };
|
|
65
|
+
const blocks = Math.min(byWarps, byRegisters, bySharedMemory, byBlocks, byBarriers);
|
|
66
|
+
const order = ['warps', 'registers', 'sharedMemory', 'blocks', 'barriers'];
|
|
67
|
+
return { blocks, limiters: order.filter((name) => limits[name] === blocks), limits, warpsPerBlock: warps, activeWarps: blocks * warps, occupancy: (blocks * warps) / warpCapacity };
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=cuda-occupancy.js.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ruRE CUDA launch plan, part 2 (ADR-501): what `analyzeCuda` found, turned into a plan for measuring. For each kernel of each architecture: the
|
|
3
|
+
* theoretical occupancy at a set of block sizes, what limits it, and a few plain flags; and, on request, a small CUDA driver-API program (as text)
|
|
4
|
+
* that a person can build and run to check those numbers on their own GPU. Pure: data in, data and text out. Nothing here compiles, loads or runs
|
|
5
|
+
* GPU code, and the harness text holds nothing taken from the analyzed file (the kernel name and the file are arguments to the program).
|
|
6
|
+
*/
|
|
7
|
+
import type { CudaInfo } from './cuda-parse.js';
|
|
8
|
+
import { type Limiter, type Provenance } from './cuda-occupancy.js';
|
|
9
|
+
export declare const DEFAULT_BLOCK_SIZES: readonly number[];
|
|
10
|
+
export declare const MAX_BLOCK_SIZES = 24;
|
|
11
|
+
export declare const MAX_DYNAMIC_SHARED_BYTES: number;
|
|
12
|
+
export declare const MAX_PLANNED_KERNELS = 200;
|
|
13
|
+
export interface PlanOptions {
|
|
14
|
+
/** Only this architecture (`sm_86`). */
|
|
15
|
+
arch?: string;
|
|
16
|
+
blockSizes?: readonly number[];
|
|
17
|
+
/** Dynamic shared memory per block the launch will ask for; it is not in the file, so it is a parameter. */
|
|
18
|
+
dynamicSharedBytes?: number;
|
|
19
|
+
includeHarness?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export type PlanFlag = 'local-memory' | 'register-limited' | 'shared-memory-limited';
|
|
22
|
+
export interface PlannedBlockSize {
|
|
23
|
+
blockSize: number;
|
|
24
|
+
activeBlocksPerSM: number;
|
|
25
|
+
activeWarpsPerSM: number;
|
|
26
|
+
/** 0 to 1, four places. */
|
|
27
|
+
occupancy: number;
|
|
28
|
+
limitedBy: Limiter[];
|
|
29
|
+
}
|
|
30
|
+
export interface PlannedKernel {
|
|
31
|
+
name: string;
|
|
32
|
+
identifier: string | null;
|
|
33
|
+
/** `staticSharedBytes` is the figure the driver and occupancy calculation use; `sharedSectionBytes` is what the file's section says (cuobjdump's SHARED), which differs on sm_90 and later. */
|
|
34
|
+
resources: {
|
|
35
|
+
registers: number | null;
|
|
36
|
+
staticSharedBytes: number;
|
|
37
|
+
sharedSectionBytes: number;
|
|
38
|
+
localBytes: number;
|
|
39
|
+
constantBytes: number | null;
|
|
40
|
+
codeBytes: number | null;
|
|
41
|
+
};
|
|
42
|
+
/** Absent when the file gave no register count: occupancy cannot be worked out without it. */
|
|
43
|
+
blockSizes?: PlannedBlockSize[];
|
|
44
|
+
best?: {
|
|
45
|
+
blockSizes: number[];
|
|
46
|
+
occupancy: number;
|
|
47
|
+
limitedBy: Limiter[];
|
|
48
|
+
};
|
|
49
|
+
flags: PlanFlag[];
|
|
50
|
+
note?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface PlannedArch {
|
|
53
|
+
arch: string;
|
|
54
|
+
/** False when there is no limits table for this architecture; then there are no kernels below. */
|
|
55
|
+
supported: boolean;
|
|
56
|
+
provenance: Provenance | null;
|
|
57
|
+
limits?: {
|
|
58
|
+
maxThreadsPerSM: number;
|
|
59
|
+
maxBlocksPerSM: number;
|
|
60
|
+
regsPerSM: number;
|
|
61
|
+
sharedPerSM: number;
|
|
62
|
+
sharedReservedPerBlock: number;
|
|
63
|
+
};
|
|
64
|
+
kernels: PlannedKernel[];
|
|
65
|
+
note?: string;
|
|
66
|
+
}
|
|
67
|
+
export interface LaunchPlan {
|
|
68
|
+
archs: PlannedArch[];
|
|
69
|
+
blockSizes: number[];
|
|
70
|
+
dynamicSharedBytes: number;
|
|
71
|
+
assumptions: string[];
|
|
72
|
+
harness?: string;
|
|
73
|
+
truncated: boolean;
|
|
74
|
+
}
|
|
75
|
+
export declare const ASSUMPTIONS: readonly string[];
|
|
76
|
+
export declare function planCuda(info: CudaInfo, options?: PlanOptions): LaunchPlan;
|
|
77
|
+
/**
|
|
78
|
+
* A program a person can build and run. It is text, fixed in this file: it contains nothing from the analyzed file. Run as
|
|
79
|
+
* ./occ_check <file.cubin|file.fatbin> <kernel name as listed> [dynamicSharedBytes]
|
|
80
|
+
* it asks the CUDA driver how many blocks fit per multiprocessor at each block size (compare with the plan), and, if built with -DRURE_BENCH and a
|
|
81
|
+
* rure_bench.h of your own that sets up the kernel's arguments, times launches. It loads the file's code onto the GPU; the benchmark mode runs it.
|
|
82
|
+
*/
|
|
83
|
+
export declare const HARNESS_SOURCE = "// occ_check.cpp: written by ruflo's rure_cuda_launch_plan. ruRE did not build or run this; building and running it is your decision.\n// Build: nvcc -O2 -o occ_check occ_check.cpp -lcuda (add -DRURE_BENCH with your own rure_bench.h to time launches)\n// Run: ./occ_check <file.cubin|file.fatbin> <kernel name> [dynamicSharedBytes]\n// Without RURE_BENCH it loads the code onto device 0 and asks the driver for occupancy; it launches nothing.\n// With RURE_BENCH it LAUNCHES THE KERNEL with the arguments your rure_bench.h gives it. Only do that for code you trust, on a GPU you can spare.\n#include <cuda.h>\n#include <algorithm>\n#include <cstdio>\n#include <cstdlib>\n#include <fstream>\n#include <iterator>\n#include <vector>\n#ifdef RURE_BENCH\n// rure_bench.h must define:\n// bool rure_prepare(void*** kernelParams); allocate buffers, fill an array of pointers to the kernel's arguments\n// unsigned rure_grid(int blockSize); how many blocks to launch at this block size\n// void rure_release();\n#include \"rure_bench.h\"\n#endif\n\n#define CK(call) do { CUresult r_ = (call); if (r_ != CUDA_SUCCESS) { const char* m_ = nullptr; cuGetErrorString(r_, &m_); fprintf(stderr, \"line %d: %s\\n\", __LINE__, m_ ? m_ : \"error\"); return 1; } } while (0)\n\nint main(int argc, char** argv) {\n if (argc < 3) { fprintf(stderr, \"usage: %s <file.cubin|file.fatbin> <kernel name> [dynamicSharedBytes]\\n\", argv[0]); return 2; }\n size_t dyn = argc > 3 ? strtoull(argv[3], nullptr, 10) : 0;\n std::ifstream in(argv[1], std::ios::binary);\n std::vector<char> image((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());\n if (image.empty()) { fprintf(stderr, \"cannot read %s\\n\", argv[1]); return 2; }\n\n CK(cuInit(0));\n CUdevice dev; CK(cuDeviceGet(&dev, 0));\n CUcontext ctx; CK(cuDevicePrimaryCtxRetain(&ctx, dev)); CK(cuCtxSetCurrent(ctx));\n CUmodule mod; CK(cuModuleLoadData(&mod, image.data()));\n CUfunction fn; CK(cuModuleGetFunction(&fn, mod, argv[2]));\n\n int regs, shared, local, maxThreads, maxThreadsSM, major, minor, blocksSM, regsSM, sharedSM, reserved, optin;\n CK(cuFuncGetAttribute(®s, CU_FUNC_ATTRIBUTE_NUM_REGS, fn));\n CK(cuFuncGetAttribute(&shared, CU_FUNC_ATTRIBUTE_SHARED_SIZE_BYTES, fn));\n CK(cuFuncGetAttribute(&local, CU_FUNC_ATTRIBUTE_LOCAL_SIZE_BYTES, fn));\n CK(cuFuncGetAttribute(&maxThreads, CU_FUNC_ATTRIBUTE_MAX_THREADS_PER_BLOCK, fn));\n CK(cuDeviceGetAttribute(&maxThreadsSM, CU_DEVICE_ATTRIBUTE_MAX_THREADS_PER_MULTIPROCESSOR, dev));\n CK(cuDeviceGetAttribute(&major, CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR, dev));\n CK(cuDeviceGetAttribute(&minor, CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR, dev));\n CK(cuDeviceGetAttribute(&blocksSM, CU_DEVICE_ATTRIBUTE_MAX_BLOCKS_PER_MULTIPROCESSOR, dev));\n CK(cuDeviceGetAttribute(®sSM, CU_DEVICE_ATTRIBUTE_MAX_REGISTERS_PER_MULTIPROCESSOR, dev));\n CK(cuDeviceGetAttribute(&sharedSM, CU_DEVICE_ATTRIBUTE_MAX_SHARED_MEMORY_PER_MULTIPROCESSOR, dev));\n CK(cuDeviceGetAttribute(&reserved, CU_DEVICE_ATTRIBUTE_RESERVED_SHARED_MEMORY_PER_BLOCK, dev));\n CK(cuDeviceGetAttribute(&optin, CU_DEVICE_ATTRIBUTE_MAX_SHARED_MEMORY_PER_BLOCK_OPTIN, dev));\n printf(\"device sm_%d%d: maxThreadsPerSM %d, maxBlocksPerSM %d, regsPerSM %d, sharedPerSM %d, sharedReservedPerBlock %d, sharedPerBlockOptin %d\\n\", major, minor, maxThreadsSM, blocksSM, regsSM, sharedSM, reserved, optin);\n printf(\"kernel: registers %d, static shared %d, local %d, max threads per block %d\\n\", regs, shared, local, maxThreads);\n printf(\"blockSize activeBlocksPerSM activeWarpsPerSM occupancy%s\\n\",\n#ifdef RURE_BENCH\n \" medianMs\");\n#else\n \"\");\n#endif\n\n const int sizes[] = {32, 64, 96, 128, 192, 256, 384, 512, 768, 1024};\n for (int b : sizes) {\n int blocks = 0;\n if (cuOccupancyMaxActiveBlocksPerMultiprocessor(&blocks, fn, b, dyn) != CUDA_SUCCESS) blocks = 0;\n int warps = blocks * ((b + 31) / 32);\n printf(\"%d %d %d %.4f\", b, blocks, warps, (double)warps / (maxThreadsSM / 32));\n#ifdef RURE_BENCH\n double ms = -1;\n if (blocks > 0) {\n void** params = nullptr;\n if (!rure_prepare(¶ms)) { fprintf(stderr, \"rure_prepare failed\\n\"); return 1; }\n CUevent t0, t1; CK(cuEventCreate(&t0, CU_EVENT_DEFAULT)); CK(cuEventCreate(&t1, CU_EVENT_DEFAULT));\n std::vector<float> times;\n for (int i = 0; i < 23; i++) {\n CK(cuEventRecord(t0, 0));\n CK(cuLaunchKernel(fn, rure_grid(b), 1, 1, b, 1, 1, (unsigned)dyn, 0, params, 0));\n CK(cuEventRecord(t1, 0)); CK(cuEventSynchronize(t1));\n float t; CK(cuEventElapsedTime(&t, t0, t1));\n if (i >= 3) times.push_back(t);\n }\n std::sort(times.begin(), times.end());\n ms = times[times.size() / 2];\n rure_release();\n }\n printf(\" %.4f\", ms);\n#endif\n printf(\"\\n\");\n }\n return 0;\n}\n";
|
|
84
|
+
//# sourceMappingURL=cuda-plan.d.ts.map
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { limitsFor, occupancy } from './cuda-occupancy.js';
|
|
2
|
+
export const DEFAULT_BLOCK_SIZES = [32, 64, 96, 128, 192, 256, 384, 512, 768, 1024];
|
|
3
|
+
export const MAX_BLOCK_SIZES = 24;
|
|
4
|
+
export const MAX_DYNAMIC_SHARED_BYTES = 1024 * 1024;
|
|
5
|
+
export const MAX_PLANNED_KERNELS = 200;
|
|
6
|
+
export const ASSUMPTIONS = [
|
|
7
|
+
'Theoretical occupancy: how many warps can be resident on one multiprocessor given the kernel\'s registers and static shared memory. It bounds, and does not predict, performance.',
|
|
8
|
+
'Static shared memory is the amount in the file, less the 1 KiB the driver reserves per block where the compiler folds it into that figure (sm_90 and later); a kernel with no static shared section uses none. Dynamic shared memory is not in the file, so it is the dynamicSharedBytes parameter (default 0), the same for every kernel.',
|
|
9
|
+
'Default shared-memory carveout and cache configuration, one block barrier per block (the default), no cluster launch, and no opt-in to more than 48 KiB of shared memory per block.',
|
|
10
|
+
'The registers in the file are what the compiler chose for that architecture; the driver may JIT PTX to different numbers.',
|
|
11
|
+
'Running the plan is a separate step that you take; nothing here compiles or runs GPU code.',
|
|
12
|
+
];
|
|
13
|
+
const round4 = (n) => Math.round(n * 10_000) / 10_000;
|
|
14
|
+
function cleanSizes(sizes) {
|
|
15
|
+
const wanted = sizes === undefined ? [...DEFAULT_BLOCK_SIZES] : [...sizes];
|
|
16
|
+
if (wanted.length === 0 || wanted.length > MAX_BLOCK_SIZES)
|
|
17
|
+
throw new RangeError(`blockSizes must hold 1 to ${MAX_BLOCK_SIZES} sizes`);
|
|
18
|
+
for (const size of wanted)
|
|
19
|
+
if (!Number.isInteger(size) || size < 1 || size > 1024)
|
|
20
|
+
throw new RangeError('each block size must be an integer from 1 to 1024');
|
|
21
|
+
return [...new Set(wanted)].sort((a, b) => a - b);
|
|
22
|
+
}
|
|
23
|
+
function planKernel(kernel, arch, sizes, dynamic) {
|
|
24
|
+
const limits = limitsFor(arch);
|
|
25
|
+
const sectionShared = kernel.sharedBytes ?? 0;
|
|
26
|
+
const staticShared = Math.max(0, sectionShared - (limits.cubinSharedIncludesReserve ? limits.sharedReservedPerBlock : 0));
|
|
27
|
+
const local = kernel.localBytes ?? 0;
|
|
28
|
+
const out = {
|
|
29
|
+
name: kernel.name,
|
|
30
|
+
identifier: kernel.identifier,
|
|
31
|
+
resources: { registers: kernel.registers, staticSharedBytes: staticShared, sharedSectionBytes: sectionShared, localBytes: local, constantBytes: kernel.constantBytes, codeBytes: kernel.codeBytes },
|
|
32
|
+
flags: [],
|
|
33
|
+
};
|
|
34
|
+
if (local > 0)
|
|
35
|
+
out.flags.push('local-memory');
|
|
36
|
+
if (kernel.registers === null || kernel.registers < 1) {
|
|
37
|
+
out.note = 'the file holds no register count for this kernel, so occupancy cannot be worked out';
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
out.blockSizes = sizes.map((blockSize) => {
|
|
41
|
+
const r = occupancy(limits, { registers: kernel.registers, staticSharedBytes: staticShared, dynamicSharedBytes: dynamic, blockSize });
|
|
42
|
+
return { blockSize, activeBlocksPerSM: r.blocks, activeWarpsPerSM: r.activeWarps, occupancy: round4(r.occupancy), limitedBy: r.limiters };
|
|
43
|
+
});
|
|
44
|
+
const top = Math.max(...out.blockSizes.map((b) => b.occupancy));
|
|
45
|
+
const best = out.blockSizes.filter((b) => b.occupancy === top);
|
|
46
|
+
if (top > 0) {
|
|
47
|
+
// The limiters at the smallest best size are the ones to report: a larger one only repeats them.
|
|
48
|
+
const first = best[0];
|
|
49
|
+
out.best = { blockSizes: best.map((b) => b.blockSize), occupancy: top, limitedBy: first.limitedBy };
|
|
50
|
+
if (top < 1 && first.limitedBy.includes('registers'))
|
|
51
|
+
out.flags.push('register-limited');
|
|
52
|
+
if (top < 1 && first.limitedBy.includes('sharedMemory'))
|
|
53
|
+
out.flags.push('shared-memory-limited');
|
|
54
|
+
}
|
|
55
|
+
else
|
|
56
|
+
out.note = 'no listed block size can launch with these resources';
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
export function planCuda(info, options = {}) {
|
|
60
|
+
const sizes = cleanSizes(options.blockSizes);
|
|
61
|
+
const dynamic = options.dynamicSharedBytes ?? 0;
|
|
62
|
+
if (!Number.isInteger(dynamic) || dynamic < 0 || dynamic > MAX_DYNAMIC_SHARED_BYTES)
|
|
63
|
+
throw new RangeError(`dynamicSharedBytes must be an integer from 0 to ${MAX_DYNAMIC_SHARED_BYTES}`);
|
|
64
|
+
const wanted = options.arch === undefined ? info.cubinArchs : [options.arch];
|
|
65
|
+
const archs = [];
|
|
66
|
+
let planned = 0;
|
|
67
|
+
let truncated = false;
|
|
68
|
+
for (const arch of wanted) {
|
|
69
|
+
const limits = limitsFor(arch);
|
|
70
|
+
if (limits === null) {
|
|
71
|
+
archs.push({ arch, supported: false, provenance: null, kernels: [], note: `no occupancy limits are tabulated for ${arch}` });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const entries = info.kernels.filter((kernel) => kernel.isEntry && kernel.arch === arch);
|
|
75
|
+
const kernels = [];
|
|
76
|
+
for (const kernel of entries) {
|
|
77
|
+
if (planned >= MAX_PLANNED_KERNELS) {
|
|
78
|
+
truncated = true;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
kernels.push(planKernel(kernel, arch, sizes, dynamic));
|
|
82
|
+
planned++;
|
|
83
|
+
}
|
|
84
|
+
archs.push({
|
|
85
|
+
arch,
|
|
86
|
+
supported: true,
|
|
87
|
+
provenance: limits.provenance,
|
|
88
|
+
limits: { maxThreadsPerSM: limits.maxThreadsPerSM, maxBlocksPerSM: limits.maxBlocksPerSM, regsPerSM: limits.regsPerSM, sharedPerSM: limits.sharedPerSM, sharedReservedPerBlock: limits.sharedReservedPerBlock },
|
|
89
|
+
kernels,
|
|
90
|
+
...(entries.length === 0 && { note: `${arch} is in the file but holds no launchable kernel` }),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const noCubins = info.cubinArchs.length === 0;
|
|
94
|
+
return {
|
|
95
|
+
archs,
|
|
96
|
+
blockSizes: sizes,
|
|
97
|
+
dynamicSharedBytes: dynamic,
|
|
98
|
+
assumptions: [...ASSUMPTIONS, ...(noCubins ? ['The file holds no compiled code for any architecture (PTX only, or nothing readable), so there are no register counts to plan from.'] : [])],
|
|
99
|
+
...(options.includeHarness === true && { harness: HARNESS_SOURCE }),
|
|
100
|
+
truncated,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* A program a person can build and run. It is text, fixed in this file: it contains nothing from the analyzed file. Run as
|
|
105
|
+
* ./occ_check <file.cubin|file.fatbin> <kernel name as listed> [dynamicSharedBytes]
|
|
106
|
+
* it asks the CUDA driver how many blocks fit per multiprocessor at each block size (compare with the plan), and, if built with -DRURE_BENCH and a
|
|
107
|
+
* rure_bench.h of your own that sets up the kernel's arguments, times launches. It loads the file's code onto the GPU; the benchmark mode runs it.
|
|
108
|
+
*/
|
|
109
|
+
export const HARNESS_SOURCE = `// occ_check.cpp: written by ruflo's rure_cuda_launch_plan. ruRE did not build or run this; building and running it is your decision.
|
|
110
|
+
// Build: nvcc -O2 -o occ_check occ_check.cpp -lcuda (add -DRURE_BENCH with your own rure_bench.h to time launches)
|
|
111
|
+
// Run: ./occ_check <file.cubin|file.fatbin> <kernel name> [dynamicSharedBytes]
|
|
112
|
+
// Without RURE_BENCH it loads the code onto device 0 and asks the driver for occupancy; it launches nothing.
|
|
113
|
+
// With RURE_BENCH it LAUNCHES THE KERNEL with the arguments your rure_bench.h gives it. Only do that for code you trust, on a GPU you can spare.
|
|
114
|
+
#include <cuda.h>
|
|
115
|
+
#include <algorithm>
|
|
116
|
+
#include <cstdio>
|
|
117
|
+
#include <cstdlib>
|
|
118
|
+
#include <fstream>
|
|
119
|
+
#include <iterator>
|
|
120
|
+
#include <vector>
|
|
121
|
+
#ifdef RURE_BENCH
|
|
122
|
+
// rure_bench.h must define:
|
|
123
|
+
// bool rure_prepare(void*** kernelParams); allocate buffers, fill an array of pointers to the kernel's arguments
|
|
124
|
+
// unsigned rure_grid(int blockSize); how many blocks to launch at this block size
|
|
125
|
+
// void rure_release();
|
|
126
|
+
#include "rure_bench.h"
|
|
127
|
+
#endif
|
|
128
|
+
|
|
129
|
+
#define CK(call) do { CUresult r_ = (call); if (r_ != CUDA_SUCCESS) { const char* m_ = nullptr; cuGetErrorString(r_, &m_); fprintf(stderr, "line %d: %s\\n", __LINE__, m_ ? m_ : "error"); return 1; } } while (0)
|
|
130
|
+
|
|
131
|
+
int main(int argc, char** argv) {
|
|
132
|
+
if (argc < 3) { fprintf(stderr, "usage: %s <file.cubin|file.fatbin> <kernel name> [dynamicSharedBytes]\\n", argv[0]); return 2; }
|
|
133
|
+
size_t dyn = argc > 3 ? strtoull(argv[3], nullptr, 10) : 0;
|
|
134
|
+
std::ifstream in(argv[1], std::ios::binary);
|
|
135
|
+
std::vector<char> image((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
|
136
|
+
if (image.empty()) { fprintf(stderr, "cannot read %s\\n", argv[1]); return 2; }
|
|
137
|
+
|
|
138
|
+
CK(cuInit(0));
|
|
139
|
+
CUdevice dev; CK(cuDeviceGet(&dev, 0));
|
|
140
|
+
CUcontext ctx; CK(cuDevicePrimaryCtxRetain(&ctx, dev)); CK(cuCtxSetCurrent(ctx));
|
|
141
|
+
CUmodule mod; CK(cuModuleLoadData(&mod, image.data()));
|
|
142
|
+
CUfunction fn; CK(cuModuleGetFunction(&fn, mod, argv[2]));
|
|
143
|
+
|
|
144
|
+
int regs, shared, local, maxThreads, maxThreadsSM, major, minor, blocksSM, regsSM, sharedSM, reserved, optin;
|
|
145
|
+
CK(cuFuncGetAttribute(®s, CU_FUNC_ATTRIBUTE_NUM_REGS, fn));
|
|
146
|
+
CK(cuFuncGetAttribute(&shared, CU_FUNC_ATTRIBUTE_SHARED_SIZE_BYTES, fn));
|
|
147
|
+
CK(cuFuncGetAttribute(&local, CU_FUNC_ATTRIBUTE_LOCAL_SIZE_BYTES, fn));
|
|
148
|
+
CK(cuFuncGetAttribute(&maxThreads, CU_FUNC_ATTRIBUTE_MAX_THREADS_PER_BLOCK, fn));
|
|
149
|
+
CK(cuDeviceGetAttribute(&maxThreadsSM, CU_DEVICE_ATTRIBUTE_MAX_THREADS_PER_MULTIPROCESSOR, dev));
|
|
150
|
+
CK(cuDeviceGetAttribute(&major, CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR, dev));
|
|
151
|
+
CK(cuDeviceGetAttribute(&minor, CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR, dev));
|
|
152
|
+
CK(cuDeviceGetAttribute(&blocksSM, CU_DEVICE_ATTRIBUTE_MAX_BLOCKS_PER_MULTIPROCESSOR, dev));
|
|
153
|
+
CK(cuDeviceGetAttribute(®sSM, CU_DEVICE_ATTRIBUTE_MAX_REGISTERS_PER_MULTIPROCESSOR, dev));
|
|
154
|
+
CK(cuDeviceGetAttribute(&sharedSM, CU_DEVICE_ATTRIBUTE_MAX_SHARED_MEMORY_PER_MULTIPROCESSOR, dev));
|
|
155
|
+
CK(cuDeviceGetAttribute(&reserved, CU_DEVICE_ATTRIBUTE_RESERVED_SHARED_MEMORY_PER_BLOCK, dev));
|
|
156
|
+
CK(cuDeviceGetAttribute(&optin, CU_DEVICE_ATTRIBUTE_MAX_SHARED_MEMORY_PER_BLOCK_OPTIN, dev));
|
|
157
|
+
printf("device sm_%d%d: maxThreadsPerSM %d, maxBlocksPerSM %d, regsPerSM %d, sharedPerSM %d, sharedReservedPerBlock %d, sharedPerBlockOptin %d\\n", major, minor, maxThreadsSM, blocksSM, regsSM, sharedSM, reserved, optin);
|
|
158
|
+
printf("kernel: registers %d, static shared %d, local %d, max threads per block %d\\n", regs, shared, local, maxThreads);
|
|
159
|
+
printf("blockSize activeBlocksPerSM activeWarpsPerSM occupancy%s\\n",
|
|
160
|
+
#ifdef RURE_BENCH
|
|
161
|
+
" medianMs");
|
|
162
|
+
#else
|
|
163
|
+
"");
|
|
164
|
+
#endif
|
|
165
|
+
|
|
166
|
+
const int sizes[] = {32, 64, 96, 128, 192, 256, 384, 512, 768, 1024};
|
|
167
|
+
for (int b : sizes) {
|
|
168
|
+
int blocks = 0;
|
|
169
|
+
if (cuOccupancyMaxActiveBlocksPerMultiprocessor(&blocks, fn, b, dyn) != CUDA_SUCCESS) blocks = 0;
|
|
170
|
+
int warps = blocks * ((b + 31) / 32);
|
|
171
|
+
printf("%d %d %d %.4f", b, blocks, warps, (double)warps / (maxThreadsSM / 32));
|
|
172
|
+
#ifdef RURE_BENCH
|
|
173
|
+
double ms = -1;
|
|
174
|
+
if (blocks > 0) {
|
|
175
|
+
void** params = nullptr;
|
|
176
|
+
if (!rure_prepare(¶ms)) { fprintf(stderr, "rure_prepare failed\\n"); return 1; }
|
|
177
|
+
CUevent t0, t1; CK(cuEventCreate(&t0, CU_EVENT_DEFAULT)); CK(cuEventCreate(&t1, CU_EVENT_DEFAULT));
|
|
178
|
+
std::vector<float> times;
|
|
179
|
+
for (int i = 0; i < 23; i++) {
|
|
180
|
+
CK(cuEventRecord(t0, 0));
|
|
181
|
+
CK(cuLaunchKernel(fn, rure_grid(b), 1, 1, b, 1, 1, (unsigned)dyn, 0, params, 0));
|
|
182
|
+
CK(cuEventRecord(t1, 0)); CK(cuEventSynchronize(t1));
|
|
183
|
+
float t; CK(cuEventElapsedTime(&t, t0, t1));
|
|
184
|
+
if (i >= 3) times.push_back(t);
|
|
185
|
+
}
|
|
186
|
+
std::sort(times.begin(), times.end());
|
|
187
|
+
ms = times[times.size() / 2];
|
|
188
|
+
rure_release();
|
|
189
|
+
}
|
|
190
|
+
printf(" %.4f", ms);
|
|
191
|
+
#endif
|
|
192
|
+
printf("\\n");
|
|
193
|
+
}
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
`;
|
|
197
|
+
//# sourceMappingURL=cuda-plan.js.map
|
|
@@ -584,7 +584,12 @@ export class HeadlessWorkerExecutor extends EventEmitter {
|
|
|
584
584
|
return false;
|
|
585
585
|
}
|
|
586
586
|
clearTimeout(entry.timeout);
|
|
587
|
-
|
|
587
|
+
if (entry.abort) {
|
|
588
|
+
entry.abort();
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
this.killEntryTree(entry.process, 'SIGTERM');
|
|
592
|
+
}
|
|
588
593
|
this.processPool.delete(executionId);
|
|
589
594
|
this.emit('cancelled', { executionId });
|
|
590
595
|
// Process next in queue
|
|
@@ -600,15 +605,21 @@ export class HeadlessWorkerExecutor extends EventEmitter {
|
|
|
600
605
|
const entries = Array.from(this.processPool.entries());
|
|
601
606
|
for (const [executionId, entry] of entries) {
|
|
602
607
|
clearTimeout(entry.timeout);
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
608
|
+
if (entry.abort) {
|
|
609
|
+
// Settles the promise and owns the SIGTERM -> SIGKILL escalation (#4463).
|
|
610
|
+
entry.abort();
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
this.killEntryTree(entry.process, 'SIGTERM');
|
|
614
|
+
// SIGKILL fallback after 5s to prevent orphan processes (#1395 Bug 6)
|
|
615
|
+
setTimeout(() => {
|
|
616
|
+
try {
|
|
617
|
+
if (entry.process.exitCode === null && entry.process.signalCode === null)
|
|
618
|
+
this.killEntryTree(entry.process, 'SIGKILL');
|
|
619
|
+
}
|
|
620
|
+
catch { /* already dead */ }
|
|
621
|
+
}, 5000).unref();
|
|
622
|
+
}
|
|
612
623
|
this.emit('cancelled', { executionId });
|
|
613
624
|
cancelled++;
|
|
614
625
|
}
|
|
@@ -1075,16 +1086,31 @@ Analyze the above codebase context and provide your response following the forma
|
|
|
1075
1086
|
}
|
|
1076
1087
|
catch { /* already dead */ }
|
|
1077
1088
|
};
|
|
1089
|
+
// #4463 — `child.killed` only means a signal was delivered, not that the
|
|
1090
|
+
// child exited, so escalation keys off the exit state instead.
|
|
1091
|
+
const hasExited = () => child.exitCode !== null || child.signalCode !== null;
|
|
1092
|
+
let escalationHandle;
|
|
1093
|
+
let deadlineHandle;
|
|
1094
|
+
const terminate = () => {
|
|
1095
|
+
if (escalationHandle)
|
|
1096
|
+
return; // already signalled; never repeat SIGTERM
|
|
1097
|
+
killTree('SIGTERM');
|
|
1098
|
+
// Give it a moment to terminate gracefully, then force it.
|
|
1099
|
+
escalationHandle = setTimeout(() => {
|
|
1100
|
+
if (!hasExited())
|
|
1101
|
+
killTree('SIGKILL');
|
|
1102
|
+
}, 5000);
|
|
1103
|
+
};
|
|
1104
|
+
const stopTimers = () => {
|
|
1105
|
+
clearTimeout(escalationHandle);
|
|
1106
|
+
clearTimeout(deadlineHandle);
|
|
1107
|
+
};
|
|
1108
|
+
child.once('exit', () => clearTimeout(escalationHandle));
|
|
1109
|
+
child.once('close', stopTimers);
|
|
1078
1110
|
// Setup timeout
|
|
1079
1111
|
const timeoutHandle = setTimeout(() => {
|
|
1080
1112
|
if (this.processPool.has(options.executionId)) {
|
|
1081
|
-
|
|
1082
|
-
// Give it a moment to terminate gracefully
|
|
1083
|
-
setTimeout(() => {
|
|
1084
|
-
if (!child.killed) {
|
|
1085
|
-
killTree('SIGKILL');
|
|
1086
|
-
}
|
|
1087
|
-
}, 5000);
|
|
1113
|
+
terminate();
|
|
1088
1114
|
}
|
|
1089
1115
|
}, options.timeoutMs);
|
|
1090
1116
|
// Track in process pool
|
|
@@ -1101,8 +1127,17 @@ Analyze the above codebase context and provide your response following the forma
|
|
|
1101
1127
|
let resolved = false;
|
|
1102
1128
|
const cleanup = () => {
|
|
1103
1129
|
clearTimeout(timeoutHandle);
|
|
1130
|
+
clearTimeout(deadlineHandle);
|
|
1104
1131
|
this.processPool.delete(options.executionId);
|
|
1105
1132
|
};
|
|
1133
|
+
poolEntry.abort = () => {
|
|
1134
|
+
if (resolved)
|
|
1135
|
+
return;
|
|
1136
|
+
resolved = true;
|
|
1137
|
+
terminate();
|
|
1138
|
+
cleanup();
|
|
1139
|
+
resolve({ success: false, output: stdout || stderr, error: 'Execution cancelled' });
|
|
1140
|
+
};
|
|
1106
1141
|
child.stdout?.on('data', (data) => {
|
|
1107
1142
|
const chunk = data.toString();
|
|
1108
1143
|
stdout += chunk;
|
|
@@ -1152,13 +1187,15 @@ Analyze the above codebase context and provide your response following the forma
|
|
|
1152
1187
|
});
|
|
1153
1188
|
});
|
|
1154
1189
|
// Handle timeout
|
|
1155
|
-
setTimeout(() => {
|
|
1190
|
+
deadlineHandle = setTimeout(() => {
|
|
1156
1191
|
if (resolved)
|
|
1157
1192
|
return;
|
|
1158
1193
|
if (!this.processPool.has(options.executionId))
|
|
1159
1194
|
return;
|
|
1160
1195
|
resolved = true;
|
|
1161
|
-
|
|
1196
|
+
// Normally a no-op: the timeout handler above already signalled the
|
|
1197
|
+
// tree and owns the SIGKILL escalation (no duplicate SIGTERM).
|
|
1198
|
+
terminate();
|
|
1162
1199
|
cleanup();
|
|
1163
1200
|
resolve({
|
|
1164
1201
|
success: false,
|
|
@@ -90,6 +90,7 @@ export declare class WorkerDaemon extends EventEmitter {
|
|
|
90
90
|
private headlessInitPromise;
|
|
91
91
|
private originalConfig?;
|
|
92
92
|
private fileConfigResourceThresholds?;
|
|
93
|
+
private fileConfigScalars?;
|
|
93
94
|
private gitIdentity;
|
|
94
95
|
private lastSupervisorRenewalMs;
|
|
95
96
|
constructor(projectRoot: string, config?: Partial<DaemonConfig>);
|
|
@@ -143,6 +143,8 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
143
143
|
// originalConfig — so a config.json override kept losing to whatever
|
|
144
144
|
// .claude-flow/daemon-state.json had persisted from a previous run.
|
|
145
145
|
fileConfigResourceThresholds;
|
|
146
|
+
// #4462 — the same record for maxConcurrent / workerTimeoutMs.
|
|
147
|
+
fileConfigScalars;
|
|
146
148
|
// #2661 root-fix — resolved once (git identity doesn't change at runtime)
|
|
147
149
|
// and reused for lease heartbeats + supervisor election, both gated on
|
|
148
150
|
// aiWorkersEnabled since they only matter for the recurring AI schedule.
|
|
@@ -161,6 +163,7 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
161
163
|
maxCpuLoad: fileConfig.maxCpuLoad,
|
|
162
164
|
minFreeMemoryPercent: fileConfig.minFreeMemoryPercent,
|
|
163
165
|
};
|
|
166
|
+
this.fileConfigScalars = { maxConcurrent: fileConfig.maxConcurrent, workerTimeoutMs: fileConfig.workerTimeoutMs };
|
|
164
167
|
// CPU-proportional smart default instead of hardcoded 2.0
|
|
165
168
|
const cpuCount = WorkerDaemon.getEffectiveCpuCount();
|
|
166
169
|
let smartMaxCpuLoad = Math.max(cpuCount * 0.8, 2.0); // Floor of 2.0 for single-CPU machines
|
|
@@ -691,10 +694,16 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
691
694
|
this.config.resourceThresholds.minFreeMemoryPercent = rt.minFreeMemoryPercent;
|
|
692
695
|
}
|
|
693
696
|
}
|
|
694
|
-
|
|
697
|
+
// #4462 — same rule for the two scalar fields: constructor arg or
|
|
698
|
+
// config.json beats saved state, which only fills in what neither set.
|
|
699
|
+
const maxConcurrentExplicit = this.originalConfig?.maxConcurrent !== undefined
|
|
700
|
+
|| this.fileConfigScalars?.maxConcurrent !== undefined;
|
|
701
|
+
const workerTimeoutExplicit = this.originalConfig?.workerTimeoutMs !== undefined
|
|
702
|
+
|| this.fileConfigScalars?.workerTimeoutMs !== undefined;
|
|
703
|
+
if (!maxConcurrentExplicit && typeof saved.config?.maxConcurrent === 'number' && saved.config.maxConcurrent > 0) {
|
|
695
704
|
this.config.maxConcurrent = saved.config.maxConcurrent;
|
|
696
705
|
}
|
|
697
|
-
if (typeof saved.config?.workerTimeoutMs === 'number' && saved.config.workerTimeoutMs > 0) {
|
|
706
|
+
if (!workerTimeoutExplicit && typeof saved.config?.workerTimeoutMs === 'number' && saved.config.workerTimeoutMs > 0) {
|
|
698
707
|
this.config.workerTimeoutMs = saved.config.workerTimeoutMs;
|
|
699
708
|
}
|
|
700
709
|
// Restore worker runtime states (runCount, successCount, etc.)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.62.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|