@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 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, model, hardware, deviceCount, runtime, quantization,
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,
@@ -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;
@@ -1,4 +1,4 @@
1
- /*! ML Bottleneck engine v0.5.0 | https://mlbottleneck.com | MIT */
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.0";
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.0";
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.0 | https://mlbottleneck.com | MIT */
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.0";
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.0" };
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.0",
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",