@mlbottleneck/engine 0.5.0 → 0.5.1
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 +2 -2
- package/mlbottleneck-engine.d.ts +4 -0
- package/mlbottleneck-engine.mjs +33 -7
- package/mlbottleneck-engine.umd.js +33 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,8 +80,8 @@ const { ceiling } = engine.predict({ model: 'qwen3.6_35b_a3b', hardware: 'AMD St
|
|
|
80
80
|
memory: { modelSizeGB, residentWeightsGB, kvCacheGB, availableGB },
|
|
81
81
|
measured: { nearest, // closest stock measurement on this model + hardware template (community gold run or lab stock row), or null
|
|
82
82
|
labTuned }, // closest tuned neural.download lab run on the same machine (same template and device count), or null
|
|
83
|
-
// each: { tokensPerSecond, origin: 'community'|'lab', stack,
|
|
84
|
-
// depthTokens, speculation, sameSetup, url, note }
|
|
83
|
+
// each: { tokensPerSecond (per request), aggregateTokensPerSecond, concurrency, origin: 'community'|'lab', stack,
|
|
84
|
+
// model, hardware, deviceCount, runtime, quantization, depthTokens, speculation, sameSetup, url, note }
|
|
85
85
|
bottleneck: 'memory' | 'compute' | 'runtime' | 'coordination' | ..., // devices[].coreBinding adds 'attention' for deep contexts
|
|
86
86
|
power: { watts, tdpWatts, costPerDay, costPer1KTokens },
|
|
87
87
|
devices: [{ name, template, residentWeightGB, kvCacheGB, hasOverflow, overflowMode,
|
package/mlbottleneck-engine.d.ts
CHANGED
|
@@ -105,6 +105,10 @@ export interface MeasuredRun {
|
|
|
105
105
|
runtime: string;
|
|
106
106
|
quantization: string;
|
|
107
107
|
depthTokens: number;
|
|
108
|
+
/** Concurrent requests in the measured run (always equals the request's batch size). */
|
|
109
|
+
concurrency: number;
|
|
110
|
+
/** tokensPerSecond is per request; this is the combined rate across the batch. */
|
|
111
|
+
aggregateTokensPerSecond: number;
|
|
108
112
|
speculation: { method: string; tokens: number | null } | null;
|
|
109
113
|
/** Same runtime, quantization family, and device count as the request. */
|
|
110
114
|
sameSetup: boolean;
|
package/mlbottleneck-engine.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! ML Bottleneck engine v0.5.
|
|
1
|
+
/*! ML Bottleneck engine v0.5.1 | https://mlbottleneck.com | MIT */
|
|
2
2
|
// Generated by scripts/build-sdk.mjs from engine.js and sdk/api.js. Do not edit.
|
|
3
3
|
|
|
4
4
|
function createEngine(options = {}) {
|
|
@@ -7384,6 +7384,7 @@ function createEngine(options = {}) {
|
|
|
7384
7384
|
const speculation = overrides.speculation !== undefined ? overrides.speculation : (row.speculation || null);
|
|
7385
7385
|
const observedTokS = Number.isFinite(overrides.observedTokS) ? overrides.observedTokS : row.observedTokS;
|
|
7386
7386
|
if (!Number.isFinite(observedTokS) || observedTokS <= 0) return null;
|
|
7387
|
+
const batchSize = Math.max(1, Number(overrides.batchSize) || 1);
|
|
7387
7388
|
const deviceCount = Math.max(1, Number(row.deviceCount) || 1);
|
|
7388
7389
|
const runtime = FRAMEWORK_PROFILES[row.runtimeKey];
|
|
7389
7390
|
const strategy = row.strategy || (deviceCount > 1 ? (TENSOR_CAPABLE_RUNTIMES.includes(row.runtimeKey) ? 'tensor' : 'pipeline') : 'pipeline');
|
|
@@ -7411,7 +7412,11 @@ function createEngine(options = {}) {
|
|
|
7411
7412
|
outputTokens,
|
|
7412
7413
|
contextLength: promptTokens + outputTokens,
|
|
7413
7414
|
decodeContextTokens: promptTokens + outputTokens / 2,
|
|
7415
|
+
// Per-request decode rate (one sequence's tokens per second); the
|
|
7416
|
+
// aggregate across a batched run is kept separately.
|
|
7414
7417
|
observedTokS,
|
|
7418
|
+
batchSize,
|
|
7419
|
+
aggregateTokS: Number.isFinite(overrides.aggregateTokS) ? overrides.aggregateTokS : observedTokS * batchSize,
|
|
7415
7420
|
prefillTokS: Number.isFinite(overrides.prefillTokS) ? overrides.prefillTokS : (Number.isFinite(row.prefillTokS) ? row.prefillTokS : null),
|
|
7416
7421
|
reproducibility: 1
|
|
7417
7422
|
};
|
|
@@ -7441,6 +7446,19 @@ function createEngine(options = {}) {
|
|
|
7441
7446
|
});
|
|
7442
7447
|
if (stepped) rows.push(stepped);
|
|
7443
7448
|
}
|
|
7449
|
+
// Concurrency sweeps: { users, perUserTokS, aggregateTokS } per level
|
|
7450
|
+
// (llama-batched-bench -npl / vllm bench serve --max-concurrency).
|
|
7451
|
+
for (const level of Array.isArray(row.concurrencySweep) ? row.concurrencySweep : []) {
|
|
7452
|
+
const users = Math.max(1, Number(level.users) || 1);
|
|
7453
|
+
const perUser = Number.isFinite(level.perUserTokS) ? level.perUserTokS : (Number.isFinite(level.aggregateTokS) ? level.aggregateTokS / users : NaN);
|
|
7454
|
+
const batched = normalizeLabEvidenceRow(row, {
|
|
7455
|
+
id: `${row.id}@u${users}`,
|
|
7456
|
+
batchSize: users,
|
|
7457
|
+
observedTokS: perUser,
|
|
7458
|
+
aggregateTokS: Number.isFinite(level.aggregateTokS) ? level.aggregateTokS : perUser * users
|
|
7459
|
+
});
|
|
7460
|
+
if (batched) rows.push(batched);
|
|
7461
|
+
}
|
|
7444
7462
|
}
|
|
7445
7463
|
labEvidenceCache = rows;
|
|
7446
7464
|
return rows;
|
|
@@ -7458,6 +7476,12 @@ function createEngine(options = {}) {
|
|
|
7458
7476
|
(row.deviceCount || 1) === (target.deviceCount || 1);
|
|
7459
7477
|
}
|
|
7460
7478
|
|
|
7479
|
+
// A reference run must serve the same number of concurrent requests as
|
|
7480
|
+
// the plan: per-request decode at 16 users says nothing about one user.
|
|
7481
|
+
function sameBatch(target, row) {
|
|
7482
|
+
return (row.batchSize || 1) === (target.batchSize || 1);
|
|
7483
|
+
}
|
|
7484
|
+
|
|
7461
7485
|
function depthDistance(target, row) {
|
|
7462
7486
|
const targetDepth = Math.max(64, Number.isFinite(target.decodeContextTokens) ? target.decodeContextTokens : (target.contextLength || 64));
|
|
7463
7487
|
const rowDepth = Math.max(64, Number.isFinite(row.decodeContextTokens) ? row.decodeContextTokens : (row.contextLength || 64));
|
|
@@ -7470,7 +7494,7 @@ function createEngine(options = {}) {
|
|
|
7470
7494
|
function findNearestMeasuredRun(target) {
|
|
7471
7495
|
if (!target?.presetKey || !target.hardwareTemplate) return null;
|
|
7472
7496
|
const candidates = getMeasuredReferenceRows()
|
|
7473
|
-
.filter(row => row.presetKey === target.presetKey && row.hardwareTemplate === target.hardwareTemplate)
|
|
7497
|
+
.filter(row => row.presetKey === target.presetKey && row.hardwareTemplate === target.hardwareTemplate && sameBatch(target, row))
|
|
7474
7498
|
.map(row => ({ row, score: getGoldSimilarity(target, row) }))
|
|
7475
7499
|
.sort((a, b) => b.score - a.score ||
|
|
7476
7500
|
depthDistance(target, a.row) - depthDistance(target, b.row) ||
|
|
@@ -7489,7 +7513,7 @@ function createEngine(options = {}) {
|
|
|
7489
7513
|
if (!target?.presetKey || !target.hardwareTemplate) return null;
|
|
7490
7514
|
const candidates = getLabEvidenceRows()
|
|
7491
7515
|
.filter(row => row.stack === 'tuned' && row.presetKey === target.presetKey &&
|
|
7492
|
-
row.hardwareTemplate === target.hardwareTemplate && (row.deviceCount || 1) === (target.deviceCount || 1))
|
|
7516
|
+
row.hardwareTemplate === target.hardwareTemplate && (row.deviceCount || 1) === (target.deviceCount || 1) && sameBatch(target, row))
|
|
7493
7517
|
.map(row => ({ row, score: getGoldSimilarity(target, row) }))
|
|
7494
7518
|
.sort((a, b) => b.score - a.score ||
|
|
7495
7519
|
depthDistance(target, a.row) - depthDistance(target, b.row) ||
|
|
@@ -7545,13 +7569,13 @@ function createEngine(options = {}) {
|
|
|
7545
7569
|
specAcceptance: null,
|
|
7546
7570
|
specDraftRatio: spec && Number.isFinite(spec.draftRatio) ? spec.draftRatio : null,
|
|
7547
7571
|
kvCacheCompression: 'none',
|
|
7548
|
-
batchSize: 1,
|
|
7572
|
+
batchSize: row.batchSize || 1,
|
|
7549
7573
|
promptTokens: row.promptTokens,
|
|
7550
7574
|
outputTokens: row.outputTokens,
|
|
7551
7575
|
seqLength: row.promptTokens + row.outputTokens
|
|
7552
7576
|
});
|
|
7553
7577
|
const metrics = calculateMetricsForConfig(modelConfig, devices);
|
|
7554
|
-
const genericTokS = calculateSystemRateFromDeviceRates(metrics.map(metric => metric.decodeTokensPerSecond), strategy, 1, devices, getSystemRateOptions(modelConfig));
|
|
7578
|
+
const genericTokS = calculateSystemRateFromDeviceRates(metrics.map(metric => metric.decodeTokensPerSecond), strategy, row.batchSize || 1, devices, getSystemRateOptions(modelConfig));
|
|
7555
7579
|
const prefillProjectedTokS = getSystemPrefillRateForMetrics(modelConfig, metrics, devices, strategy);
|
|
7556
7580
|
const calibration = calculateCurrentCalibration(modelConfig, metrics, genericTokS, strategy, devices);
|
|
7557
7581
|
if (!calibration || !Number.isFinite(genericTokS) || genericTokS <= 0) return null;
|
|
@@ -7973,6 +7997,8 @@ function createEngine(options = {}) {
|
|
|
7973
7997
|
runtime: row.runtimeKey,
|
|
7974
7998
|
quantization: row.quantization || row.quantKey,
|
|
7975
7999
|
depthTokens: Math.round(row.decodeContextTokens || row.contextLength || 0),
|
|
8000
|
+
concurrency: row.batchSize || 1,
|
|
8001
|
+
aggregateTokensPerSecond: sdkRound(row.aggregateTokS || row.observedTokS * (row.batchSize || 1), 2),
|
|
7976
8002
|
speculation: row.speculation ? { method: row.speculation.method, tokens: row.speculation.tokens ?? null } : null,
|
|
7977
8003
|
sameSetup: Boolean(row.sameSetup),
|
|
7978
8004
|
url: row.source || row.url || null,
|
|
@@ -8041,11 +8067,11 @@ function createEngine(options = {}) {
|
|
|
8041
8067
|
|
|
8042
8068
|
if (options.snapshot) setEngineEvidence(options.snapshot);
|
|
8043
8069
|
const api = createApi();
|
|
8044
|
-
api.version = "0.5.
|
|
8070
|
+
api.version = "0.5.1";
|
|
8045
8071
|
api.evidenceGeneratedAt = options.snapshot?.generatedAt || null;
|
|
8046
8072
|
return api;
|
|
8047
8073
|
}
|
|
8048
8074
|
|
|
8049
|
-
const version = "0.5.
|
|
8075
|
+
const version = "0.5.1";
|
|
8050
8076
|
export { createEngine, version };
|
|
8051
8077
|
export default createEngine;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! ML Bottleneck engine v0.5.
|
|
1
|
+
/*! ML Bottleneck engine v0.5.1 | https://mlbottleneck.com | MIT */
|
|
2
2
|
// Generated by scripts/build-sdk.mjs from engine.js and sdk/api.js. Do not edit.
|
|
3
3
|
(function (root, factory) {
|
|
4
4
|
if (typeof define === 'function' && define.amd) {
|
|
@@ -7393,6 +7393,7 @@
|
|
|
7393
7393
|
const speculation = overrides.speculation !== undefined ? overrides.speculation : (row.speculation || null);
|
|
7394
7394
|
const observedTokS = Number.isFinite(overrides.observedTokS) ? overrides.observedTokS : row.observedTokS;
|
|
7395
7395
|
if (!Number.isFinite(observedTokS) || observedTokS <= 0) return null;
|
|
7396
|
+
const batchSize = Math.max(1, Number(overrides.batchSize) || 1);
|
|
7396
7397
|
const deviceCount = Math.max(1, Number(row.deviceCount) || 1);
|
|
7397
7398
|
const runtime = FRAMEWORK_PROFILES[row.runtimeKey];
|
|
7398
7399
|
const strategy = row.strategy || (deviceCount > 1 ? (TENSOR_CAPABLE_RUNTIMES.includes(row.runtimeKey) ? 'tensor' : 'pipeline') : 'pipeline');
|
|
@@ -7420,7 +7421,11 @@
|
|
|
7420
7421
|
outputTokens,
|
|
7421
7422
|
contextLength: promptTokens + outputTokens,
|
|
7422
7423
|
decodeContextTokens: promptTokens + outputTokens / 2,
|
|
7424
|
+
// Per-request decode rate (one sequence's tokens per second); the
|
|
7425
|
+
// aggregate across a batched run is kept separately.
|
|
7423
7426
|
observedTokS,
|
|
7427
|
+
batchSize,
|
|
7428
|
+
aggregateTokS: Number.isFinite(overrides.aggregateTokS) ? overrides.aggregateTokS : observedTokS * batchSize,
|
|
7424
7429
|
prefillTokS: Number.isFinite(overrides.prefillTokS) ? overrides.prefillTokS : (Number.isFinite(row.prefillTokS) ? row.prefillTokS : null),
|
|
7425
7430
|
reproducibility: 1
|
|
7426
7431
|
};
|
|
@@ -7450,6 +7455,19 @@
|
|
|
7450
7455
|
});
|
|
7451
7456
|
if (stepped) rows.push(stepped);
|
|
7452
7457
|
}
|
|
7458
|
+
// Concurrency sweeps: { users, perUserTokS, aggregateTokS } per level
|
|
7459
|
+
// (llama-batched-bench -npl / vllm bench serve --max-concurrency).
|
|
7460
|
+
for (const level of Array.isArray(row.concurrencySweep) ? row.concurrencySweep : []) {
|
|
7461
|
+
const users = Math.max(1, Number(level.users) || 1);
|
|
7462
|
+
const perUser = Number.isFinite(level.perUserTokS) ? level.perUserTokS : (Number.isFinite(level.aggregateTokS) ? level.aggregateTokS / users : NaN);
|
|
7463
|
+
const batched = normalizeLabEvidenceRow(row, {
|
|
7464
|
+
id: `${row.id}@u${users}`,
|
|
7465
|
+
batchSize: users,
|
|
7466
|
+
observedTokS: perUser,
|
|
7467
|
+
aggregateTokS: Number.isFinite(level.aggregateTokS) ? level.aggregateTokS : perUser * users
|
|
7468
|
+
});
|
|
7469
|
+
if (batched) rows.push(batched);
|
|
7470
|
+
}
|
|
7453
7471
|
}
|
|
7454
7472
|
labEvidenceCache = rows;
|
|
7455
7473
|
return rows;
|
|
@@ -7467,6 +7485,12 @@
|
|
|
7467
7485
|
(row.deviceCount || 1) === (target.deviceCount || 1);
|
|
7468
7486
|
}
|
|
7469
7487
|
|
|
7488
|
+
// A reference run must serve the same number of concurrent requests as
|
|
7489
|
+
// the plan: per-request decode at 16 users says nothing about one user.
|
|
7490
|
+
function sameBatch(target, row) {
|
|
7491
|
+
return (row.batchSize || 1) === (target.batchSize || 1);
|
|
7492
|
+
}
|
|
7493
|
+
|
|
7470
7494
|
function depthDistance(target, row) {
|
|
7471
7495
|
const targetDepth = Math.max(64, Number.isFinite(target.decodeContextTokens) ? target.decodeContextTokens : (target.contextLength || 64));
|
|
7472
7496
|
const rowDepth = Math.max(64, Number.isFinite(row.decodeContextTokens) ? row.decodeContextTokens : (row.contextLength || 64));
|
|
@@ -7479,7 +7503,7 @@
|
|
|
7479
7503
|
function findNearestMeasuredRun(target) {
|
|
7480
7504
|
if (!target?.presetKey || !target.hardwareTemplate) return null;
|
|
7481
7505
|
const candidates = getMeasuredReferenceRows()
|
|
7482
|
-
.filter(row => row.presetKey === target.presetKey && row.hardwareTemplate === target.hardwareTemplate)
|
|
7506
|
+
.filter(row => row.presetKey === target.presetKey && row.hardwareTemplate === target.hardwareTemplate && sameBatch(target, row))
|
|
7483
7507
|
.map(row => ({ row, score: getGoldSimilarity(target, row) }))
|
|
7484
7508
|
.sort((a, b) => b.score - a.score ||
|
|
7485
7509
|
depthDistance(target, a.row) - depthDistance(target, b.row) ||
|
|
@@ -7498,7 +7522,7 @@
|
|
|
7498
7522
|
if (!target?.presetKey || !target.hardwareTemplate) return null;
|
|
7499
7523
|
const candidates = getLabEvidenceRows()
|
|
7500
7524
|
.filter(row => row.stack === 'tuned' && row.presetKey === target.presetKey &&
|
|
7501
|
-
row.hardwareTemplate === target.hardwareTemplate && (row.deviceCount || 1) === (target.deviceCount || 1))
|
|
7525
|
+
row.hardwareTemplate === target.hardwareTemplate && (row.deviceCount || 1) === (target.deviceCount || 1) && sameBatch(target, row))
|
|
7502
7526
|
.map(row => ({ row, score: getGoldSimilarity(target, row) }))
|
|
7503
7527
|
.sort((a, b) => b.score - a.score ||
|
|
7504
7528
|
depthDistance(target, a.row) - depthDistance(target, b.row) ||
|
|
@@ -7554,13 +7578,13 @@
|
|
|
7554
7578
|
specAcceptance: null,
|
|
7555
7579
|
specDraftRatio: spec && Number.isFinite(spec.draftRatio) ? spec.draftRatio : null,
|
|
7556
7580
|
kvCacheCompression: 'none',
|
|
7557
|
-
batchSize: 1,
|
|
7581
|
+
batchSize: row.batchSize || 1,
|
|
7558
7582
|
promptTokens: row.promptTokens,
|
|
7559
7583
|
outputTokens: row.outputTokens,
|
|
7560
7584
|
seqLength: row.promptTokens + row.outputTokens
|
|
7561
7585
|
});
|
|
7562
7586
|
const metrics = calculateMetricsForConfig(modelConfig, devices);
|
|
7563
|
-
const genericTokS = calculateSystemRateFromDeviceRates(metrics.map(metric => metric.decodeTokensPerSecond), strategy, 1, devices, getSystemRateOptions(modelConfig));
|
|
7587
|
+
const genericTokS = calculateSystemRateFromDeviceRates(metrics.map(metric => metric.decodeTokensPerSecond), strategy, row.batchSize || 1, devices, getSystemRateOptions(modelConfig));
|
|
7564
7588
|
const prefillProjectedTokS = getSystemPrefillRateForMetrics(modelConfig, metrics, devices, strategy);
|
|
7565
7589
|
const calibration = calculateCurrentCalibration(modelConfig, metrics, genericTokS, strategy, devices);
|
|
7566
7590
|
if (!calibration || !Number.isFinite(genericTokS) || genericTokS <= 0) return null;
|
|
@@ -7982,6 +8006,8 @@
|
|
|
7982
8006
|
runtime: row.runtimeKey,
|
|
7983
8007
|
quantization: row.quantization || row.quantKey,
|
|
7984
8008
|
depthTokens: Math.round(row.decodeContextTokens || row.contextLength || 0),
|
|
8009
|
+
concurrency: row.batchSize || 1,
|
|
8010
|
+
aggregateTokensPerSecond: sdkRound(row.aggregateTokS || row.observedTokS * (row.batchSize || 1), 2),
|
|
7985
8011
|
speculation: row.speculation ? { method: row.speculation.method, tokens: row.speculation.tokens ?? null } : null,
|
|
7986
8012
|
sameSetup: Boolean(row.sameSetup),
|
|
7987
8013
|
url: row.source || row.url || null,
|
|
@@ -8050,10 +8076,10 @@
|
|
|
8050
8076
|
|
|
8051
8077
|
if (options.snapshot) setEngineEvidence(options.snapshot);
|
|
8052
8078
|
const api = createApi();
|
|
8053
|
-
api.version = "0.5.
|
|
8079
|
+
api.version = "0.5.1";
|
|
8054
8080
|
api.evidenceGeneratedAt = options.snapshot?.generatedAt || null;
|
|
8055
8081
|
return api;
|
|
8056
8082
|
}
|
|
8057
8083
|
|
|
8058
|
-
return { createEngine, version: "0.5.
|
|
8084
|
+
return { createEngine, version: "0.5.1" };
|
|
8059
8085
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mlbottleneck/engine",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Physics-based LLM inference planner: decode/prefill tokens per second, memory fit, multi-GPU strategy, and speculative-decoding gains for any model on any hardware, calibrated on community benchmarks.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|