> ## Documentation Index
> Fetch the complete documentation index at: https://docs.poolside.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Capacity planning

> Use the capacity planner to estimate concurrent-agent capacity and developer seat counts for a Poolside deployment.

export const CapacityCalculator = () => {
  const {useState, useEffect, useRef} = React;
  const DTYPE_BYTES = {
    fp4: 0.5,
    fp8: 1.0,
    bf16: 2.0
  };
  const FRAMEWORK_OVERHEAD = 2.0 * 1024 ** 3;
  const ACTIVATION_SCRATCH = 1.5 * 1024 ** 3;
  function hostKvOffloadBytes(hw, nGpus) {
    if (hw.unifiedMemory) return 0;
    return Math.floor(0.75 * nGpus * hw.hbmCap * 1024 ** 3);
  }
  const SERVING_OVERHEAD_S = 0.05;
  const PREFILL_CHUNK = 8192;
  const STAGGER_S = 1.0;
  const MAX_SIM_S = 600.0;
  const RNG_SEED = 0;
  const SPEC_ACCEPTANCE = 3.85;
  const PCIE_GEN5_GBPS = 64.0;
  const H100 = {
    name: 'H100-80GB',
    arch: 'hopper',
    sm: 90,
    peakFp8: 1979,
    peakBf16: 989,
    peakFp4: 0,
    hbmCap: 80,
    hbmBw: 3.35e12,
    fastBw: 450,
    fastDomain: 8,
    fastHops: 1,
    fastSwitched: true,
    hasFlash: true,
    tileM: 16,
    sramPerCore: 0,
    numCores: 1,
    supportsFp4: false,
    tuning: {
      prefillTpOverlap: 0.40,
      prefillCompilerOverhead: 0.97,
      decodeBwUtil: 0.85,
      decodeKernelLaunchUs: 5.0,
      decodePerSessionOverheadUs: 0.0,
      decodeCommLatencyUs: 2.0,
      prefillBatchPenaltyRate: 0.03,
      quantFp4: 0.93,
      quantFp8: 0.97
    }
  };
  const H200 = {
    name: 'H200-141GB',
    arch: 'hopper',
    sm: 90,
    peakFp8: 1979,
    peakBf16: 989,
    peakFp4: 0,
    hbmCap: 141,
    hbmBw: 4.8e12,
    fastBw: 450,
    fastDomain: 8,
    fastHops: 1,
    fastSwitched: true,
    hasFlash: true,
    tileM: 16,
    sramPerCore: 0,
    numCores: 1,
    supportsFp4: false,
    tuning: {
      prefillTpOverlap: 0.40,
      prefillCompilerOverhead: 0.97,
      decodeBwUtil: 0.85,
      decodeKernelLaunchUs: 22.0,
      decodePerSessionOverheadUs: 75.0,
      decodeCommLatencyUs: 2.0,
      prefillBatchPenaltyRate: 0.03,
      quantFp4: 0.93,
      quantFp8: 0.97
    }
  };
  const RTX4 = {
    name: 'RTX-PRO-6000-Blackwell',
    arch: 'blackwell',
    sm: 120,
    peakFp8: 1000,
    peakBf16: 500,
    peakFp4: 0,
    hbmCap: 96,
    hbmBw: 1.792e12,
    fastBw: 35,
    fastDomain: 4,
    fastHops: 2,
    fastSwitched: true,
    hasFlash: true,
    tileM: 16,
    sramPerCore: 0,
    numCores: 1,
    supportsFp4: true,
    tuning: {
      prefillTpOverlap: 0.40,
      prefillCompilerOverhead: 0.97,
      decodeBwUtil: 0.85,
      decodeKernelLaunchUs: 5.0,
      decodePerSessionOverheadUs: 0.0,
      decodeCommLatencyUs: 2.0,
      prefillBatchPenaltyRate: 0.03,
      quantFp4: 0.93,
      quantFp8: 0.97
    }
  };
  const A100_40 = {
    name: 'A100-40GB',
    arch: 'ampere',
    sm: 80,
    peakFp8: 0,
    peakBf16: 312,
    peakFp4: 0,
    hbmCap: 40,
    hbmBw: 1555 * 0.85 * 1e9,
    fastBw: 600,
    fastDomain: 8,
    fastHops: 1,
    fastSwitched: true,
    hasFlash: true,
    tileM: 16,
    sramPerCore: 0,
    numCores: 1,
    supportsFp4: false,
    tuning: {
      prefillTpOverlap: 0.35,
      prefillCompilerOverhead: 0.95,
      decodeBwUtil: 0.85,
      decodeKernelLaunchUs: 5.0,
      decodePerSessionOverheadUs: 0.0,
      decodeCommLatencyUs: 15.0,
      prefillBatchPenaltyRate: 0.03,
      quantFp4: 0.93,
      quantFp8: 0.97
    }
  };
  const A100_80 = {
    name: 'A100-80GB',
    arch: 'ampere',
    sm: 80,
    peakFp8: 0,
    peakBf16: 312,
    peakFp4: 0,
    hbmCap: 80,
    hbmBw: 2039 * 0.85 * 1e9,
    fastBw: 600,
    fastDomain: 8,
    fastHops: 1,
    fastSwitched: true,
    hasFlash: true,
    tileM: 16,
    sramPerCore: 0,
    numCores: 1,
    supportsFp4: false,
    tuning: {
      prefillTpOverlap: 0.35,
      prefillCompilerOverhead: 0.95,
      decodeBwUtil: 0.85,
      decodeKernelLaunchUs: 5.0,
      decodePerSessionOverheadUs: 0.0,
      decodeCommLatencyUs: 15.0,
      prefillBatchPenaltyRate: 0.03,
      quantFp4: 0.93,
      quantFp8: 0.97
    }
  };
  const L40S = {
    name: 'L40S',
    arch: 'ada',
    sm: 89,
    peakFp8: 733,
    peakBf16: 362,
    peakFp4: 0,
    hbmCap: 48,
    hbmBw: 864 * 0.70 * 1e9,
    fastBw: 40,
    fastDomain: 8,
    fastHops: 1,
    fastSwitched: true,
    hasFlash: true,
    tileM: 16,
    sramPerCore: 0,
    numCores: 1,
    supportsFp4: false,
    tuning: {
      prefillTpOverlap: 0.0,
      prefillCompilerOverhead: 0.95,
      decodeBwUtil: 0.85,
      decodeKernelLaunchUs: 14.2,
      decodePerSessionOverheadUs: 2000.0,
      decodeCommLatencyUs: 80.0,
      prefillBatchPenaltyRate: 0.03,
      quantFp4: 0.93,
      quantFp8: 0.97
    }
  };
  const MI300X = {
    name: 'MI300X',
    arch: 'cdna3',
    sm: 0,
    peakFp8: 2614,
    peakBf16: 1307,
    peakFp4: 0,
    hbmCap: 192,
    hbmBw: 5300 * 0.90 * 1e9,
    fastBw: 896,
    fastDomain: 8,
    fastHops: 1,
    fastSwitched: true,
    hasFlash: true,
    tileM: 16,
    sramPerCore: 0,
    numCores: 1,
    supportsFp4: false,
    tuning: {
      prefillTpOverlap: 0.30,
      prefillCompilerOverhead: 0.90,
      decodeBwUtil: 0.80,
      decodeKernelLaunchUs: 5.0,
      decodePerSessionOverheadUs: 0.0,
      decodeCommLatencyUs: 15.0,
      prefillBatchPenaltyRate: 0.04,
      quantFp4: 0.93,
      quantFp8: 0.95
    }
  };
  const SPARK = {
    name: 'DGX-Spark',
    arch: 'blackwell-lpddr5x',
    sm: 121,
    peakFp8: 250,
    peakBf16: 125,
    peakFp4: 500,
    hbmCap: 128,
    hbmBw: 273 * 0.55 * 1e9,
    fastBw: 40,
    fastDomain: 8,
    fastHops: 1,
    fastSwitched: true,
    hasFlash: true,
    tileM: 16,
    sramPerCore: 0,
    numCores: 1,
    supportsFp4: true,
    unifiedMemory: true,
    tuning: {
      prefillTpOverlap: 0.0,
      prefillCompilerOverhead: 0.95,
      decodeBwUtil: 0.85,
      decodeKernelLaunchUs: 11.0,
      decodePerSessionOverheadUs: 4000.0,
      decodeCommLatencyUs: 80.0,
      prefillBatchPenaltyRate: 0.03,
      quantFp4: 0.93,
      quantFp8: 0.97
    }
  };
  const XS2_ATTN = [...Array(10).fill('full'), ...Array(30).fill('sliding')];
  const XS2 = {
    name: 'Laguna-XS.2 (33B/3B MoE+SWA)',
    totalParamsB: 33437681664 / 1e9,
    activeParamsB: 3012200448 / 1e9,
    totalParamsBytes: 33437681664,
    numLayers: 40,
    hiddenDim: 2048,
    intermediateDim: 8192,
    moeIntermediateDim: 512,
    numExperts: 256,
    expertsPerToken: 8,
    nDenseLayers: 1,
    numAttnHeads: 48,
    numKvHeads: 8,
    headDim: 128,
    attentionPattern: XS2_ATTN,
    slidingWindowSize: 512,
    slidingWindowLayers: 30,
    ffnGateProj: true
  };
  const M1_ATTN = Array(70).fill('full');
  const M1 = {
    name: 'Laguna-M.1 (222B/23B MoE)',
    totalParamsB: 223.0,
    activeParamsB: 21.0,
    totalParamsBytes: 223446880256,
    numLayers: 70,
    hiddenDim: 4096,
    intermediateDim: 16384,
    moeIntermediateDim: 1024,
    numExperts: 256,
    expertsPerToken: 16,
    nDenseLayers: 3,
    numAttnHeads: 64,
    numKvHeads: 8,
    headDim: 128,
    attentionPattern: M1_ATTN,
    slidingWindowSize: 0,
    slidingWindowLayers: 0,
    ffnGateProj: true
  };
  const CALIB_ROWS = [{
    model: 'Laguna-XS.2 (33B/3B MoE+SWA)',
    gpu: 'RTX-PRO-6000-Blackwell',
    nGpus: 4,
    arch: 'blackwell',
    sm: 120,
    wDtype: 'fp8',
    kvDtype: 'fp8',
    points: [{
      b: 8,
      mfu: 0.4265,
      mbu: 0.0574
    }, {
      b: 12,
      mfu: 0.4655,
      mbu: 0.0759
    }, {
      b: 16,
      mfu: 0.4070,
      mbu: 0.0733
    }, {
      b: 24,
      mfu: 0.3264,
      mbu: 0.0784
    }, {
      b: 32,
      mfu: 0.3426,
      mbu: 0.0903
    }, {
      b: 40,
      mfu: 0.3469,
      mbu: 0.1148
    }, {
      b: 48,
      mfu: 0.2744,
      mbu: 0.1166
    }],
    mfuLow: 0.4265,
    mbuLow: 0.0574,
    mfuHigh: 0.3248,
    mbuHigh: 0.0858
  }, {
    model: 'Laguna-M.1 (222B/23B MoE)',
    gpu: 'RTX-PRO-6000-Blackwell',
    nGpus: 4,
    arch: 'blackwell',
    sm: 120,
    wDtype: 'fp4',
    kvDtype: 'fp8',
    points: [{
      b: 8,
      mfu: 0.2762,
      mbu: 0.0569
    }, {
      b: 12,
      mfu: 0.3247,
      mbu: 0.0764
    }, {
      b: 16,
      mfu: 0.3551,
      mbu: 0.0810
    }, {
      b: 24,
      mfu: 0.3010,
      mbu: 0.0759
    }, {
      b: 32,
      mfu: 0.2851,
      mbu: 0.0764
    }, {
      b: 40,
      mfu: 0.2160,
      mbu: 0.0803
    }, {
      b: 48,
      mfu: 0.1802,
      mbu: 0.0714
    }],
    mfuLow: 0.2762,
    mbuLow: 0.0569,
    mfuHigh: 0.3104,
    mbuHigh: 0.0762
  }, {
    model: 'Laguna-XS.2 (33B/3B MoE+SWA)',
    gpu: 'H200-141GB',
    nGpus: 4,
    arch: 'hopper',
    sm: 90,
    wDtype: 'fp8',
    kvDtype: 'fp8',
    points: [{
      b: 8,
      mfu: 0.2069,
      mbu: 0.4612
    }, {
      b: 12,
      mfu: 0.1676,
      mbu: 0.4428
    }, {
      b: 16,
      mfu: 0.1569,
      mbu: 0.4203
    }, {
      b: 24,
      mfu: 0.1110,
      mbu: 0.3704
    }, {
      b: 32,
      mfu: 0.0924,
      mbu: 0.3547
    }, {
      b: 40,
      mfu: 0.1155,
      mbu: 0.3373
    }, {
      b: 48,
      mfu: 0.0689,
      mbu: 0.2866
    }],
    mfuLow: 0.2069,
    mbuLow: 0.4612,
    mfuHigh: 0.1129,
    mbuHigh: 0.3558
  }, {
    model: 'Laguna-M.1 (222B/23B MoE)',
    gpu: 'H200-141GB',
    nGpus: 8,
    arch: 'hopper',
    sm: 90,
    wDtype: 'fp8',
    kvDtype: 'fp8',
    points: [{
      b: 8,
      mfu: 0.2061,
      mbu: 0.4634
    }, {
      b: 12,
      mfu: 0.1729,
      mbu: 0.4294
    }, {
      b: 16,
      mfu: 0.3071,
      mbu: 0.4730
    }, {
      b: 24,
      mfu: 0.1625,
      mbu: 0.3942
    }, {
      b: 32,
      mfu: 0.1182,
      mbu: 0.3735
    }, {
      b: 40,
      mfu: 0.1359,
      mbu: 0.3675
    }, {
      b: 48,
      mfu: 0.0878,
      mbu: 0.3190
    }],
    mfuLow: 0.2061,
    mbuLow: 0.4634,
    mfuHigh: 0.2015,
    mbuHigh: 0.3866
  }, {
    model: 'Laguna-XS.2 (33B/3B MoE+SWA)',
    gpu: 'L40S',
    nGpus: 4,
    arch: 'ada',
    sm: 89,
    wDtype: 'fp8',
    kvDtype: 'fp8',
    points: [{
      b: 1,
      mfu: 2.2139,
      mbu: 0.5150
    }, {
      b: 2,
      mfu: 0.2138,
      mbu: 0.3627
    }, {
      b: 4,
      mfu: 0.1967,
      mbu: 0.2655
    }, {
      b: 8,
      mfu: 0.1491,
      mbu: 0.1709
    }, {
      b: 12,
      mfu: 0.1168,
      mbu: 0.1623
    }, {
      b: 16,
      mfu: 0.1173,
      mbu: 0.1322
    }, {
      b: 24,
      mfu: 0.0754,
      mbu: 0.0961
    }, {
      b: 32,
      mfu: 0.0661,
      mbu: 0.0726
    }, {
      b: 40,
      mfu: 0.0499,
      mbu: 0.0653
    }, {
      b: 56,
      mfu: 0.0453,
      mbu: 0.0475
    }],
    mfuLow: 0.1491,
    mbuLow: 0.1709,
    mfuHigh: 0.0708,
    mbuHigh: 0.0844
  }, {
    model: 'Laguna-XS.2 (33B/3B MoE+SWA)',
    gpu: 'DGX-Spark',
    nGpus: 1,
    arch: 'blackwell-lpddr5x',
    sm: 121,
    wDtype: 'fp8',
    kvDtype: 'fp8',
    points: [{
      b: 1,
      mfu: 2.1566,
      mbu: 0.5310
    }, {
      b: 2,
      mfu: 0.2804,
      mbu: 0.3648
    }, {
      b: 4,
      mfu: 0.2747,
      mbu: 0.2960
    }, {
      b: 8,
      mfu: 0.0470,
      mbu: 0.3476
    }, {
      b: 12,
      mfu: 0.0251,
      mbu: 0.4643
    }, {
      b: 16,
      mfu: 0.0190,
      mbu: 0.5555
    }, {
      b: 24,
      mfu: 0.0122,
      mbu: 0.6662
    }],
    mfuLow: 0.0470,
    mbuLow: 0.3476,
    mfuHigh: 0.0156,
    mbuHigh: 0.6108
  }, {
    model: 'Laguna-M.1 (222B/23B MoE)',
    gpu: 'RTX-PRO-6000-Blackwell',
    nGpus: 4,
    arch: 'blackwell',
    sm: 120,
    wDtype: 'fp8',
    kvDtype: 'fp8',
    points: [{
      b: 8,
      mfu: 0.4149,
      mbu: 0.0585
    }, {
      b: 12,
      mfu: 0.3894,
      mbu: 0.0719
    }, {
      b: 16,
      mfu: 0.3549,
      mbu: 0.0691
    }, {
      b: 24,
      mfu: 0.0918,
      mbu: 0.0525
    }, {
      b: 32,
      mfu: 0.0380,
      mbu: 0.0645
    }, {
      b: 40,
      mfu: 0.0266,
      mbu: 0.0679
    }, {
      b: 48,
      mfu: 0.0192,
      mbu: 0.0714
    }],
    mfuLow: 0.4149,
    mbuLow: 0.0585,
    mfuHigh: 0.0918,
    mbuHigh: 0.0668
  }];
  const CALIB = {
    perSmMfu: {
      120: 0.3247,
      90: 0.1464,
      89: 0.1170,
      121: 0.0470
    },
    perSmMbu: {
      120: 0.0733,
      90: 0.3838,
      89: 0.1472,
      121: 0.4643
    },
    perSmMfuLow: {
      120: 0.3504,
      90: 0.2065,
      89: 0.1729,
      121: 0.2776
    },
    perSmMbuLow: {
      120: 0.0572,
      90: 0.4623,
      89: 0.2682,
      121: 0.3562
    },
    perSmMfuHigh: {
      120: 0.3261,
      90: 0.1393,
      89: 0.0708,
      121: 0.0156
    },
    perSmMbuHigh: {
      120: 0.0860,
      90: 0.3712,
      89: 0.0844,
      121: 0.6108
    },
    hwFactor: {
      'H200-141GB': [0.1370, 0.3821],
      'RTX-PRO-6000-Blackwell': [0.2851, 0.0764],
      'L40S': [0.1170, 0.1472],
      'DGX-Spark': [0.0470, 0.4643]
    },
    modelFactor: {
      'Laguna-M.1 (222B/23B MoE)': [1.0, 1.0],
      'Laguna-XS.2 (33B/3B MoE+SWA)': [1.0, 1.0]
    },
    hwFactorLow: {
      'H200-141GB': [0.2065, 0.4623],
      'RTX-PRO-6000-Blackwell': [0.3504, 0.0572],
      'L40S': [0.1729, 0.2682],
      'DGX-Spark': [0.2776, 0.3562]
    },
    modelFactorLow: {
      'Laguna-M.1 (222B/23B MoE)': [1.0, 1.0],
      'Laguna-XS.2 (33B/3B MoE+SWA)': [1.0, 1.0]
    },
    hwFactorHigh: {
      'H200-141GB': [0.1393, 0.3712],
      'RTX-PRO-6000-Blackwell': [0.3261, 0.0860],
      'L40S': [0.0708, 0.0844],
      'DGX-Spark': [0.0156, 0.6108]
    },
    modelFactorHigh: {
      'Laguna-M.1 (222B/23B MoE)': [1.0, 1.0],
      'Laguna-XS.2 (33B/3B MoE+SWA)': [1.0, 1.0]
    }
  };
  function getPeakTflops(hw, precision) {
    if (precision === 'fp4') {
      if (hw.peakFp4 > 0) return hw.peakFp4;
      return hw.peakFp8 > 0 ? hw.peakFp8 : hw.peakBf16;
    }
    if (precision === 'fp8') return hw.peakFp8 > 0 ? hw.peakFp8 : hw.peakBf16;
    return hw.peakBf16;
  }
  function alltoallFastBw(hw) {
    return hw.fastBw;
  }
  function computeMoeParamSplit(model) {
    const {totalParamsB, activeParamsB, numExperts, expertsPerToken} = model;
    const perExpertB = (totalParamsB - activeParamsB) / (numExperts - expertsPerToken);
    const sharedB = activeParamsB - expertsPerToken * perExpertB;
    return {
      sharedB: Math.max(0, sharedB),
      perExpertB: Math.max(0, perExpertB)
    };
  }
  function computeAttnFlopFrac(model) {
    const {hiddenDim, numKvHeads, headDim, numLayers, attentionPattern, moeIntermediateDim, intermediateDim, expertsPerToken, ffnGateProj} = model;
    const attnLayers = attentionPattern.filter(p => p === 'full' || p === 'sliding').length;
    const kvDim = numKvHeads * headDim;
    const attnProjPerLayer = 2 * hiddenDim * (2 * hiddenDim + 2 * kvDim);
    const ffnProjCount = ffnGateProj ? 3 : 2;
    const expertInter = moeIntermediateDim > 0 ? moeIntermediateDim : intermediateDim;
    const expertFfnPerLayer = 2 * expertsPerToken * ffnProjCount * hiddenDim * expertInter;
    const sharedFfn = moeIntermediateDim > 0 ? 2 * ffnProjCount * hiddenDim * intermediateDim : 0;
    const ffnPerLayer = expertFfnPerLayer + sharedFfn;
    const nonAttnLayers = numLayers - attnLayers;
    const totalAllLayers = attnLayers * (attnProjPerLayer + ffnPerLayer) + nonAttnLayers * ffnPerLayer;
    if (totalAllLayers <= 0) return 0.33;
    return attnLayers * attnProjPerLayer / totalAllLayers;
  }
  function kvTokensForLayer(layerIdx, currentPos, model) {
    const pat = model.attentionPattern[layerIdx];
    if (pat === 'sliding' && model.slidingWindowSize > 0) {
      return Math.min(currentPos, model.slidingWindowSize);
    }
    return currentPos;
  }
  function computePrefill(hw, model, cfg) {
    const {tp, promptTokens, batchSize, weightPrecision, activationPrecision, kvPrecision} = cfg;
    const physicalChips = tp;
    const peak = activationPrecision === 'bf16' ? hw.peakBf16 : getPeakTflops(hw, activationPrecision);
    const nodePeak = physicalChips * peak;
    const baseFlops = 2 * model.activeParamsB * 1e9 * promptTokens * batchSize;
    let attnScoreFlops = 0;
    const S = promptTokens;
    for (let i = 0; i < model.numLayers; i++) {
      const pat = model.attentionPattern[i];
      let W;
      if (pat === 'sliding' && model.slidingWindowSize > 0) {
        W = Math.min(S, model.slidingWindowSize);
      } else {
        W = S;
      }
      const causalSum = W * (W + 1) / 2 + Math.max(0, S - W) * W;
      attnScoreFlops += 4 * model.numAttnHeads * causalSum * model.headDim * batchSize;
    }
    const prefillFlops = baseFlops + attnScoreFlops;
    const attnFlopFrac = computeAttnFlopFrac(model);
    let eff = 1.0;
    const perExpertBatch = batchSize * promptTokens * model.expertsPerToken / model.numExperts;
    const tileUtil = Math.min(1.0, perExpertBatch / Math.max(hw.tileM, 1));
    const moeFrac = 1 - attnFlopFrac;
    eff *= moeFrac * tileUtil + (1 - moeFrac);
    eff *= 0.95;
    const kvQuant = kvPrecision === 'fp8' ? 0.98 : 1.0;
    eff *= kvQuant;
    if (weightPrecision === 'fp4') {
      eff *= hw.tuning.quantFp4 / hw.tuning.quantFp8;
    }
    if (tp > 1) {
      const ringFactor = 2 * (tp - 1) / tp;
      const arPerLayer = 2;
      const arBytes = batchSize * promptTokens * model.hiddenDim * 2 * ringFactor * arPerLayer;
      const tpBw = hw.fastBw * 1e9;
      const arTime = arBytes / Math.max(tpBw, 1);
      const layerComputeTime = prefillFlops / model.numLayers / Math.max(physicalChips * peak * 1e12, 1);
      const exposedArTime = arTime * (1 - hw.tuning.prefillTpOverlap);
      eff *= layerComputeTime / Math.max(layerComputeTime + exposedArTime, 1e-30);
    }
    if (batchSize > 1) {
      const penalty = 1.0 - hw.tuning.prefillBatchPenaltyRate * Math.log2(batchSize);
      eff *= Math.max(0.70, penalty);
    }
    eff *= 0.92 * hw.tuning.prefillCompilerOverhead;
    eff = Math.max(eff, 0.01);
    const prefillTimeS = prefillFlops / Math.max(nodePeak * 1e12 * eff, 1);
    return {
      prefillTimeMs: prefillTimeS * 1000
    };
  }
  const DECODE_W_BYTES = {
    fp4: 0.5625,
    fp8: 1.0,
    bf16: 2.0
  };
  function computeDecode(hw, model, cfg, step = 0) {
    const {tp, promptTokens, batchSize, weightPrecision, kvPrecision} = cfg;
    const physicalChips = tp;
    const wBytes = DECODE_W_BYTES[weightPrecision] ?? 1.0;
    const kvBytes = DTYPE_BYTES[kvPrecision] || 1.0;
    const currentPos = promptTokens + step;
    const {sharedB, perExpertB} = computeMoeParamSplit(model);
    const K = model.expertsPerToken, N = model.numExperts, B = Math.max(batchSize, 1);
    let activeExperts;
    if (N > K) {
      const unique = N * (1.0 - Math.pow(1.0 - K / N, B));
      activeExperts = 0.7 * K + 0.3 * unique;
    } else {
      activeExperts = N;
    }
    const activeExpertBytes = activeExperts * perExpertB * 1e9 * wBytes / (tp * 1);
    const sharedBytes = sharedB * 1e9 * wBytes / tp;
    const weightLoad = activeExpertBytes + sharedBytes;
    const kvPerToken = 2 * model.numKvHeads * model.headDim * kvBytes;
    let totalKvRead = 0;
    for (let i = 0; i < model.numLayers; i++) {
      const kv_tokens = kvTokensForLayer(i, currentPos, model);
      totalKvRead += batchSize * kv_tokens * kvPerToken / tp;
    }
    const attnLayerCount = model.attentionPattern.filter(p => p === 'full' || p === 'sliding').length;
    const kvWrite = batchSize * attnLayerCount * kvPerToken / tp;
    let totalBytes = weightLoad + totalKvRead + kvWrite;
    const kvDeqOverhead = kvPrecision === 'fp8' ? 1.02 : 1.0;
    if (kvDeqOverhead > 1.0) {
      totalBytes += totalKvRead * (kvDeqOverhead - 1.0);
    }
    const perRankBw = hw.hbmBw;
    const bwUtil = hw.tuning.decodeBwUtil;
    let commTime = 0;
    if (tp > 1) {
      const ringFactor = 2 * (tp - 1) / tp;
      const arPerLayer = 2;
      const arBytes = batchSize * model.hiddenDim * 2 * ringFactor;
      const tpBw = hw.fastBw * 1e9;
      const bwTimePerAr = arBytes / Math.max(tpBw, 1);
      const latencyFloor = hw.tuning.decodeCommLatencyUs * 1e-6;
      const perArTime = Math.max(bwTimePerAr, latencyFloor);
      commTime = model.numLayers * arPerLayer * perArTime;
    }
    let memoryTime = totalBytes / Math.max(perRankBw * bwUtil, 1) + commTime;
    if (weightPrecision === 'fp4') {
      memoryTime *= 1.47;
    }
    const peak = getPeakTflops(hw, 'fp8');
    let attnExposed = 0.0;
    if (peak > 0) {
      const attnEff = 0.90;
      let attnComputeTime = 0;
      for (let i = 0; i < model.numLayers; i++) {
        const pat = model.attentionPattern[i];
        if (pat !== 'full' && pat !== 'sliding') continue;
        const kvToks = kvTokensForLayer(i, currentPos, model);
        const layerAttnFlops = 4 * batchSize * model.numAttnHeads * kvToks * model.headDim / tp;
        attnComputeTime += layerAttnFlops / (peak * 1e12 * attnEff);
      }
      attnExposed = attnComputeTime * 0.5;
    }
    let linearComputeTime = 0;
    if (peak > 0) {
      const linearFlops = 2 * model.activeParamsB * 1e9 * batchSize;
      linearComputeTime = linearFlops / (physicalChips * peak * 1e12 * 0.85);
    }
    let perStageTime = Math.max(memoryTime, linearComputeTime) + attnExposed;
    const matmulsPerLayer = 8;
    const launchFloor = hw.tuning.decodeKernelLaunchUs * 1e-6 * matmulsPerLayer * model.numLayers;
    perStageTime = Math.max(perStageTime, launchFloor);
    const sessionOverhead = hw.tuning.decodePerSessionOverheadUs * 1e-6 * batchSize;
    perStageTime = perStageTime + sessionOverhead;
    let decodeTime = perStageTime;
    decodeTime *= 1.03;
    if (model.expertsPerToken > 1) {
      const routingImbalance = 1.0 + 0.05 * Math.log2(model.expertsPerToken);
      decodeTime *= routingImbalance;
    }
    return {
      decodeTimePerTokenMs: decodeTime * 1000
    };
  }
  function rowLookup(row, batch) {
    const pts = [...row.points].sort((a, b) => a.b - b.b);
    if (!pts.length) return [1.0, 1.0];
    if (batch === null || batch === undefined) {
      const mfus = pts.map(p => p.mfu).sort((a, b) => a - b);
      const mbus = pts.map(p => p.mbu).sort((a, b) => a - b);
      const mid = Math.floor(mfus.length / 2);
      return [mfus[mid], mbus[mid]];
    }
    if (batch <= pts[0].b) return [pts[0].mfu, pts[0].mbu];
    if (batch >= pts[pts.length - 1].b) return [pts[pts.length - 1].mfu, pts[pts.length - 1].mbu];
    for (let i = 0; i < pts.length - 1; i++) {
      if (pts[i].b <= batch && batch <= pts[i + 1].b) {
        const t = (batch - pts[i].b) / (pts[i + 1].b - pts[i].b);
        return [pts[i].mfu + t * (pts[i + 1].mfu - pts[i].mfu), pts[i].mbu + t * (pts[i + 1].mbu - pts[i].mbu)];
      }
    }
    return [pts[pts.length - 1].mfu, pts[pts.length - 1].mbu];
  }
  function _pick(lowD, highD, midD, key, def, batch) {
    if (batch === null || batch === undefined) {
      const v = midD ? midD[key] : undefined;
      return v !== undefined ? v : def;
    }
    const lo = lowD ? lowD[key] : undefined;
    const hi = highD ? highD[key] : undefined;
    const mid = midD ? midD[key] : def;
    if (lo === undefined && hi === undefined) return mid !== undefined ? mid : def;
    const loV = lo !== undefined ? lo : mid !== undefined ? mid : hi;
    const hiV = hi !== undefined ? hi : mid !== undefined ? mid : lo;
    if (batch <= 8) return loV;
    if (batch >= 16) return hiV;
    const t = (batch - 8) / 8;
    if (Array.isArray(loV)) return loV.map((l, i) => l + t * (hiV[i] - l));
    return loV + t * (hiV - loV);
  }
  function calibrationTier(hwName, sm, modelName, nGpus, wDtype, kvDtype) {
    for (const r of CALIB_ROWS) {
      if (r.model === modelName && r.gpu === hwName && r.nGpus === nGpus && (!r.wDtype || !wDtype || r.wDtype === wDtype) && (!r.kvDtype || !kvDtype || r.kvDtype === kvDtype)) return 'exact';
    }
    const sameSm = CALIB_ROWS.some(r => r.model === modelName && r.sm === sm && r.sm && (!r.wDtype || !wDtype || r.wDtype === wDtype) && (!r.kvDtype || !kvDtype || r.kvDtype === kvDtype));
    if (sameSm) return 'model_arch';
    if (CALIB.hwFactor[hwName] && CALIB.modelFactor[modelName]) return 'factorized';
    const haveSmAnchor = CALIB_ROWS.some(r => r.sm === sm);
    if (haveSmAnchor && CALIB.perSmMfu[sm] !== undefined) return 'per_arch';
    if (!haveSmAnchor) return 'uncalibrated_class';
    return 'blended';
  }
  const TIER_INFO = {
    exact: {
      label: 'Calibrated',
      band: '±20%',
      color: '#1e7e3a',
      expl: 'Direct (model, GPU, dtype) match. In-sample MAPE: ITL 8%, TTFT 21%.'
    },
    model_arch: {
      label: 'Same arch',
      band: '±25%',
      color: '#3a8a3a',
      expl: 'Same model on the same GPU architecture with matching dtypes.'
    },
    factorized: {
      label: 'Factorized',
      band: '±35%',
      color: '#b08d2a',
      expl: 'Cross-(model, GPU) extrapolation via hw_factor × model_factor. Cell-drop MAPE: ITL 15%, TTFT 36%.'
    },
    blended: {
      label: 'Partial signal',
      band: '±50%',
      color: '#cc7a1f',
      expl: 'Geometric mean of whichever factors are available — used when factorization is partial.'
    },
    per_arch: {
      label: 'SM median',
      band: '±70%',
      color: '#cc4a1f',
      expl: 'Median correction across all rows on this GPU\'s SM. Last resort — extrapolation to an unseen (model, GPU) combo.'
    },
    uncalibrated_class: {
      label: 'Uncalibrated SM',
      band: '±90%',
      color: '#aa1f1f',
      expl: 'This GPU\'s SM has no calibration anchor (e.g. A100/sm80 — no measured Ampere data). Prediction uses analytical tunings only.'
    },
    uncalibrated_extrapolation: {
      label: 'Past calibrated range',
      band: '±50%',
      color: '#cc4a1f',
      expl: 'Result exceeds 2× the largest batch in calibration. The model extrapolates the queueing slope linearly past where it was measured.'
    },
    uncalibrated: {
      label: 'Uncalibrated',
      band: 'open',
      color: '#8a8a8a',
      expl: 'Pure analytical model with no measured corrections.'
    }
  };
  function calibMults(hwName, sm, modelName, nGpus, wDtype, kvDtype, batch) {
    for (const r of CALIB_ROWS) {
      if (r.model === modelName && r.gpu === hwName && r.nGpus === nGpus && (!r.wDtype || !wDtype || r.wDtype === wDtype) && (!r.kvDtype || !kvDtype || r.kvDtype === kvDtype)) {
        return rowLookup(r, batch);
      }
    }
    const sameSm = CALIB_ROWS.filter(r => r.model === modelName && r.sm === sm && (!r.wDtype || !wDtype || r.wDtype === wDtype) && (!r.kvDtype || !kvDtype || r.kvDtype === kvDtype));
    if (sameSm.length > 0) {
      const mults = sameSm.map(r => rowLookup(r, batch));
      const mfus = mults.map(m => m[0]).sort((a, b) => a - b);
      const mbus = mults.map(m => m[1]).sort((a, b) => a - b);
      const mid = Math.floor(mfus.length / 2);
      return [mfus[mid], mbus[mid]];
    }
    const hwF = _pick(CALIB.hwFactorLow, CALIB.hwFactorHigh, CALIB.hwFactor, hwName, null, batch);
    const mF = _pick(CALIB.modelFactorLow, CALIB.modelFactorHigh, CALIB.modelFactor, modelName, null, batch);
    if (hwF && mF) return [hwF[0] * mF[0], hwF[1] * mF[1]];
    const smMfu = _pick(CALIB.perSmMfuLow, CALIB.perSmMfuHigh, CALIB.perSmMfu, sm, null, batch);
    const smMbu = _pick(CALIB.perSmMbuLow, CALIB.perSmMbuHigh, CALIB.perSmMbu, sm, null, batch);
    function blend(...vals) {
      const present = vals.filter(v => v !== null && v !== undefined && v > 0);
      if (!present.length) return 1.0;
      return Math.exp(present.reduce((s, v) => s + Math.log(v), 0) / present.length);
    }
    const mfuSig = [], mbuSig = [];
    if (hwF) {
      mfuSig.push(hwF[0]);
      mbuSig.push(hwF[1]);
    }
    if (mF) {
      mfuSig.push(mF[0]);
      mbuSig.push(mF[1]);
    }
    if (smMfu !== null && smMfu !== undefined) mfuSig.push(smMfu);
    if (smMbu !== null && smMbu !== undefined) mbuSig.push(smMbu);
    return [blend(...mfuSig), blend(...mbuSig)];
  }
  function prefillTimeS(model, hw, tp, inputTokens, wDtype, batchCtx) {
    const cfg = {
      tp,
      promptTokens: Math.max(inputTokens, 1),
      batchSize: 1,
      weightPrecision: wDtype,
      activationPrecision: wDtype,
      kvPrecision: 'fp8'
    };
    const raw = computePrefill(hw, model, cfg).prefillTimeMs / 1000;
    const [mfuMult] = calibMults(hw.name, hw.sm, model.name, tp, wDtype, 'fp8', batchCtx);
    return raw / Math.max(mfuMult, 1e-6);
  }
  function decodeTimePerTokenS(model, hw, tp, batch, avgSeqLen, wDtype, kvDtype, offloadFrac, specLen) {
    const cfg = {
      tp,
      promptTokens: Math.max(Math.floor(avgSeqLen), 1),
      batchSize: Math.max(batch, 1),
      weightPrecision: wDtype,
      kvPrecision: kvDtype
    };
    const raw = computeDecode(hw, model, cfg, 0).decodeTimePerTokenMs / 1000;
    let t = specLen > 1.0 ? raw / specLen : raw;
    if (offloadFrac > 0) {
      const bw = hw.hbmBw;
      const pcieBw = PCIE_GEN5_GBPS * 1e9;
      const slowdown = 1 - offloadFrac + offloadFrac * (bw / pcieBw);
      t *= slowdown;
    }
    const [, mbuMult] = calibMults(hw.name, hw.sm, model.name, tp, wDtype, kvDtype, batch);
    return t / Math.max(mbuMult, 1e-6);
  }
  function kvBytesForSeq(model, kvDtype, seqLen) {
    const kvBytes = DTYPE_BYTES[kvDtype] || 1.0;
    const perLayerPerToken = 2 * model.numKvHeads * model.headDim * kvBytes;
    const nSwa = Math.min(model.slidingWindowLayers, model.numLayers);
    const nFull = model.numLayers - nSwa;
    const swaEff = model.slidingWindowSize > 0 ? Math.min(seqLen, model.slidingWindowSize) : seqLen;
    return Math.floor(perLayerPerToken * (nFull * seqLen + nSwa * swaEff));
  }
  function computeBreakdown(model, hw, nGpus, wDtype, kvDtype, contextLen, nConcurrent) {
    const tp = nGpus;
    const wBytes = DTYPE_BYTES[wDtype] || 1.0;
    const wTotal = model.totalParamsBytes * wBytes;
    const wPerGpu = Math.floor(wTotal / tp);
    const kvPerSession = kvBytesForSeq(model, kvDtype, contextLen);
    const shard = model.numKvHeads >= tp ? tp : model.numKvHeads;
    const kvPerSessionPerGpu = Math.floor(kvPerSession / shard);
    const vram = hw.hbmCap * 1024 ** 3;
    const fixed = wPerGpu + FRAMEWORK_OVERHEAD + ACTIVATION_SCRATCH;
    const kvBudgetPerGpu = Math.max(0, vram - fixed);
    const hostKvPerGpu = Math.floor(hostKvOffloadBytes(hw, nGpus) / tp);
    const combinedBudget = kvBudgetPerGpu + hostKvPerGpu;
    const maxConcurrent = kvPerSessionPerGpu > 0 ? Math.floor(combinedBudget / kvPerSessionPerGpu) : 0;
    const kvNeededPerGpu = nConcurrent * kvPerSessionPerGpu;
    const overflow = Math.max(0, kvNeededPerGpu - kvBudgetPerGpu);
    const offloadFraction = kvNeededPerGpu > 0 ? overflow / kvNeededPerGpu : 0.0;
    const fits = fixed <= vram && kvNeededPerGpu <= combinedBudget;
    return {
      maxConcurrent,
      offloadFraction,
      fits,
      kvBudgetPerGpu,
      kvPerSessionPerGpu
    };
  }
  function mulberry32(seed) {
    let s = seed >>> 0;
    return function () {
      s = s + 0x6D2B79F5 >>> 0;
      let t = Math.imul(s ^ s >>> 15, 1 | s);
      t = t + Math.imul(t ^ t >>> 7, 61 | t) >>> 0;
      return ((t ^ t >>> 14) >>> 0) / 4294967296;
    };
  }
  function rngUniform(rng, n) {
    return Array.from({
      length: n
    }, () => rng());
  }
  const WF = {
    baseInputTokens: 2048,
    inputGrowthPerStep: 500,
    outputTokensPerStep: 200,
    stepsPerSession: 20,
    prefixCaching: true,
    prefixCacheHitRate: 1.0
  };
  function inputTokensForStep(stepIdx) {
    return WF.baseInputTokens + stepIdx * (WF.inputGrowthPerStep + WF.outputTokensPerStep);
  }
  function simulate(model, hw, nGpus, nSessions, contextLen, wDtype, kvDtype, specLen) {
    const tp = nGpus;
    const rng = mulberry32(RNG_SEED);
    const arrivals = rngUniform(rng, nSessions).map(v => v * STAGGER_S);
    const bd = computeBreakdown(model, hw, nGpus, wDtype, kvDtype, contextLen, nSessions);
    const offloadFraction = bd.offloadFraction;
    const prefillCache = new Map();
    const decodeCache = new Map();
    function cachedPrefill(chunk, batchCtx) {
      if (chunk <= 0) return 0;
      const key = `${chunk},${batchCtx}`;
      if (!prefillCache.has(key)) {
        prefillCache.set(key, prefillTimeS(model, hw, tp, chunk, wDtype, batchCtx));
      }
      return prefillCache.get(key);
    }
    function cachedDecode(batch, avgSeq) {
      if (batch <= 0) return 0;
      const seqBucket = Math.floor(avgSeq / 64) * 64;
      const key = `${batch},${seqBucket}`;
      if (!decodeCache.has(key)) {
        decodeCache.set(key, decodeTimePerTokenS(model, hw, tp, batch, avgSeq, wDtype, kvDtype, offloadFraction, specLen));
      }
      return decodeCache.get(key);
    }
    const sessions = arrivals.map((arr, i) => ({
      sid: i,
      arrivalTime: arr,
      nextActiveTime: arr,
      step: 0,
      state: 0,
      seqLen: 0,
      prefillRemaining: 0,
      decodeRemaining: 0,
      stepStartedAt: 0,
      lastInputTokens: 0
    }));
    let t = 0.0;
    const allLatencies = [];
    let decodeSumBatch = 0, decodePassCount = 0;
    const pendingOrThinking = [...sessions].sort((a, b) => a.nextActiveTime - b.nextActiveTime);
    const prefillQueue = [];
    const decoding = new Set();
    let decodingSeqSum = 0;
    let doneCount = 0;
    function startStep(s, now) {
      const desiredInput = inputTokensForStep(s.step);
      const hitRate = WF.prefixCacheHitRate;
      const cached = Math.floor(s.lastInputTokens * hitRate);
      const newTokens = Math.max(0, desiredInput - cached);
      if (cached === 0) s.seqLen = 0;
      s.lastInputTokens = desiredInput;
      s.prefillRemaining = newTokens;
      s.decodeRemaining = 0;
      s.state = newTokens > 0 ? 1 : 2;
      if (s.state === 2) s.decodeRemaining = WF.outputTokensPerStep;
      s.stepStartedAt = now;
    }
    function insertPending(s) {
      let lo = 0, hi = pendingOrThinking.length;
      while (lo < hi) {
        const mid = lo + hi >> 1;
        if (pendingOrThinking[mid].nextActiveTime <= s.nextActiveTime) lo = mid + 1; else hi = mid;
      }
      pendingOrThinking.splice(lo, 0, s);
    }
    function activateDue(now) {
      while (pendingOrThinking.length && pendingOrThinking[0].nextActiveTime <= now) {
        const s = pendingOrThinking.shift();
        if (s.state === 3) continue;
        startStep(s, now);
        if (s.state === 1) {
          prefillQueue.push(s);
        } else {
          decoding.add(s);
          decodingSeqSum += s.seqLen;
        }
      }
    }
    while (t < MAX_SIM_S && doneCount < nSessions) {
      activateDue(t);
      if (!prefillQueue.length && decoding.size === 0) {
        if (!pendingOrThinking.length) break;
        t = Math.max(t, pendingOrThinking[0].nextActiveTime);
        continue;
      }
      const batch = decoding.size;
      const avgSeq = batch > 0 ? decodingSeqSum / batch : 0;
      const decodeTokens = batch;
      const prefillBudget = Math.max(PREFILL_CHUNK - decodeTokens, 0);
      const prefillChunks = [];
      let totalPrefillChunk = 0;
      let remaining = prefillBudget;
      while (prefillQueue.length && remaining > 0) {
        const s = prefillQueue[0];
        const take = Math.min(remaining, s.prefillRemaining);
        if (take <= 0) break;
        prefillChunks.push({
          s,
          take
        });
        totalPrefillChunk += take;
        remaining -= take;
        if (take < s.prefillRemaining) break;
        prefillQueue.shift();
      }
      const batchCtx = batch + prefillQueue.length + prefillChunks.length;
      const tPre = cachedPrefill(totalPrefillChunk, batchCtx);
      const tDec = cachedDecode(batch, avgSeq);
      const passTime = totalPrefillChunk > 0 && batch > 0 ? tPre + 0.5 * tDec : tPre + tDec;
      if (passTime <= 0) {
        t += 1e-3;
        continue;
      }
      t += passTime;
      for (const {s, take} of prefillChunks) {
        s.prefillRemaining -= take;
        s.seqLen += take;
        if (s.prefillRemaining <= 0) {
          s.state = 2;
          s.decodeRemaining = WF.outputTokensPerStep;
          decoding.add(s);
          decodingSeqSum += s.seqLen;
        }
      }
      if (batch > 0) {
        decodeSumBatch += batch;
        decodePassCount++;
        decodingSeqSum += batch;
        const finished = [];
        for (const s of decoding) {
          s.decodeRemaining--;
          s.seqLen++;
          if (s.decodeRemaining <= 0) finished.push(s);
        }
        for (const s of finished) {
          const latency = t - s.stepStartedAt + SERVING_OVERHEAD_S;
          allLatencies.push(latency);
          decoding.delete(s);
          decodingSeqSum -= s.seqLen;
          s.step++;
          if (s.step >= WF.stepsPerSession) {
            s.state = 3;
            doneCount++;
          } else {
            const prevSeq = s.seqLen;
            startStep(s, t);
            if (s.state === 1) {
              prefillQueue.push(s);
            } else {
              decoding.add(s);
              decodingSeqSum += s.seqLen;
            }
          }
        }
      }
    }
    if (allLatencies.length === 0) return {
      p50: Infinity,
      fits: bd.fits
    };
    allLatencies.sort((a, b) => a - b);
    const p50Idx = Math.floor(allLatencies.length * 0.5);
    const p50 = allLatencies[p50Idx];
    return {
      p50,
      fits: bd.fits
    };
  }
  function tierBandFraction(tier) {
    return ({
      exact: 0.20,
      model_arch: 0.25,
      factorized: 0.35,
      blended: 0.50,
      per_arch: 0.70,
      uncalibrated_class: 0.90,
      uncalibrated_extrapolation: 0.50,
      uncalibrated: 1.00
    })[tier] ?? 1.00;
  }
  function findMaxConcurrent(model, hw, nGpus, contextLen, sloS, wDtype, kvDtype, specLen, extrapolationFactor = 2.0) {
    const bd = computeBreakdown(model, hw, nGpus, wDtype, kvDtype, contextLen, 1);
    const memCeiling = Math.max(1, bd.maxConcurrent);
    let nMax = Math.min(1024, memCeiling);
    let calibCeiling = null;
    if (Number.isFinite(extrapolationFactor)) {
      for (const r of CALIB_ROWS) {
        if (r.model === model.name && r.gpu === hw.name && r.nGpus === nGpus && (!r.wDtype || r.wDtype === wDtype) && r.points && r.points.length) {
          calibCeiling = Math.floor(Math.max(...r.points.map(p => p.b)) * extrapolationFactor);
          break;
        }
      }
    }
    if (calibCeiling) nMax = Math.min(nMax, calibCeiling);
    const res1 = simulate(model, hw, nGpus, 1, contextLen, wDtype, kvDtype, specLen);
    if (!res1.fits || res1.p50 > sloS) return {
      n: 0,
      cappedByCalibration: false
    };
    let lo = 1, hi = nMax, lastPass = 1;
    while (lo < hi) {
      const mid = Math.floor((lo + hi + 1) / 2);
      const res = simulate(model, hw, nGpus, mid, contextLen, wDtype, kvDtype, specLen);
      if (res.fits && res.p50 <= sloS) {
        lo = mid;
        lastPass = mid;
      } else {
        hi = mid - 1;
      }
    }
    const cappedByCalibration = !!(calibCeiling && lastPass >= calibCeiling);
    return {
      n: lastPass,
      cappedByCalibration
    };
  }
  function findMaxConcurrentWithBand(model, hw, nGpus, contextLen, sloS, wDtype, kvDtype, specLen) {
    const {n: maxN, cappedByCalibration} = findMaxConcurrent(model, hw, nGpus, contextLen, sloS, wDtype, kvDtype, specLen);
    let tier = calibrationTier(hw.name, hw.sm, model.name, nGpus, wDtype, kvDtype);
    if (cappedByCalibration) tier = 'uncalibrated_extrapolation';
    const band = tierBandFraction(tier);
    let nLow = maxN, nHigh = maxN;
    if (maxN > 0 && band < 1.0) {
      const sloPess = sloS / (1.0 + band);
      const sloOpt = sloS / Math.max(1.0 - band, 0.05);
      nLow = findMaxConcurrent(model, hw, nGpus, contextLen, sloPess, wDtype, kvDtype, specLen).n;
      nHigh = findMaxConcurrent(model, hw, nGpus, contextLen, sloOpt, wDtype, kvDtype, specLen, Infinity).n;
    }
    return {
      maxN,
      nLow,
      nHigh,
      tier,
      band
    };
  }
  const HARDWARE = {
    'H100': H100,
    'H200': H200,
    'RTX6000': RTX4,
    'A100-40': A100_40,
    'A100-80': A100_80,
    'L40S': L40S,
    'Spark': SPARK
  };
  function getDtypes(hw, model) {
    if (hw.peakFp8 === 0) return ['bf16', 'bf16'];
    return ['fp8', 'fp8'];
  }
  const CONTEXT_OPTIONS = [{
    label: '8K',
    value: 8192
  }, {
    label: '16K',
    value: 16384
  }, {
    label: '32K',
    value: 32768
  }, {
    label: '64K',
    value: 65536
  }, {
    label: '128K',
    value: 131072
  }, {
    label: '256K',
    value: 262144
  }];
  const SLO_OPTIONS = [{
    label: '5s',
    value: 5
  }, {
    label: '10s',
    value: 10
  }, {
    label: '20s',
    value: 20
  }, {
    label: '30s',
    value: 30
  }];
  const HW_OPTIONS = [{
    label: 'H200 (141 GB · Hopper)',
    value: 'H200'
  }, {
    label: 'H100 (80 GB · Hopper)',
    value: 'H100'
  }, {
    label: 'RTX PRO 6000 Blackwell (96 GB)',
    value: 'RTX6000'
  }, {
    label: 'A100 80 GB (Ampere · BF16)',
    value: 'A100-80'
  }, {
    label: 'A100 40 GB (Ampere · BF16)',
    value: 'A100-40'
  }, {
    label: 'L40S (48 GB · Ada · PCIe)',
    value: 'L40S'
  }, {
    label: 'NVIDIA DGX Spark (128 GB unified)',
    value: 'Spark'
  }];
  const GPU_COUNT_OPTIONS = [{
    label: '1',
    value: 1
  }, {
    label: '2',
    value: 2
  }, {
    label: '4',
    value: 4
  }, {
    label: '8',
    value: 8
  }];
  const MODEL_OPTIONS = [{
    label: 'Laguna M.1 (222B MoE)',
    value: 'M1'
  }, {
    label: 'Laguna XS.2 (33B MoE+SWA)',
    value: 'XS2'
  }];
  const MODELS = {
    M1,
    XS2
  };
  const labelStyle = {
    fontSize: 11,
    fontWeight: 600,
    color: 'var(--cc-text-muted)',
    textTransform: 'uppercase',
    letterSpacing: '0.06em'
  };
  const renderSelect = ({label, hint, value, onChange, options}) => <div key={label} style={{
    display: 'flex',
    flexDirection: 'column',
    gap: 6
  }}>
      <div style={{
    display: 'flex',
    alignItems: 'baseline',
    justifyContent: 'space-between',
    gap: 8
  }}>
        <span style={{
    ...labelStyle,
    color: t.textMuted
  }}>{label}</span>
        {hint && <span style={{
    fontSize: 11,
    color: t.textFaint
  }}>{hint}</span>}
      </div>
      <div style={{
    position: 'relative'
  }}>
        <select value={value} onChange={e => onChange(e.target.value)} style={{
    width: '100%',
    padding: '10px 32px 10px 12px',
    borderRadius: 8,
    border: `1px solid ${t.inputBorder}`,
    background: t.inputBg,
    fontSize: 13.5,
    fontWeight: 500,
    color: t.text,
    cursor: 'pointer',
    outline: 'none',
    appearance: 'none',
    WebkitAppearance: 'none',
    MozAppearance: 'none',
    transition: 'border-color 0.15s, box-shadow 0.15s',
    fontFamily: 'inherit',
    lineHeight: 1.4
  }} onFocus={e => {
    e.target.style.borderColor = t.accent;
    e.target.style.boxShadow = `0 0 0 3px ${t.accentBg}`;
  }} onBlur={e => {
    e.target.style.borderColor = t.inputBorder;
    e.target.style.boxShadow = 'none';
  }}>
          {options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
        </select>
        <svg width="10" height="6" viewBox="0 0 10 6" fill="none" style={{
    position: 'absolute',
    right: 12,
    top: '50%',
    transform: 'translateY(-50%)',
    pointerEvents: 'none'
  }}>
          <path d="M1 1L5 5L9 1" stroke={t.textMuted} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
        </svg>
      </div>
    </div>;
  const renderSegmented = ({label, hint, value, onChange, options}) => <div key={label} style={{
    display: 'flex',
    flexDirection: 'column',
    gap: 6
  }}>
      <div style={{
    display: 'flex',
    alignItems: 'baseline',
    justifyContent: 'space-between',
    gap: 8
  }}>
        <span style={{
    ...labelStyle,
    color: t.textMuted
  }}>{label}</span>
        {hint && <span style={{
    fontSize: 11,
    color: t.textFaint
  }}>{hint}</span>}
      </div>
      <div style={{
    display: 'flex',
    background: t.segBg,
    borderRadius: 8,
    padding: 3,
    gap: 2
  }}>
        {options.map(o => {
    const active = value === o.value;
    return <button key={o.value} onClick={() => onChange(o.value)} style={{
      flex: 1,
      padding: '7px 8px',
      borderRadius: 6,
      border: 'none',
      background: active ? t.segActiveBg : 'transparent',
      boxShadow: active ? dark ? '0 1px 2px rgba(0,0,0,0.4)' : '0 1px 2px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.04)' : 'none',
      fontWeight: active ? 600 : 500,
      fontSize: 13,
      color: active ? t.text : t.textMuted,
      cursor: 'pointer',
      transition: 'all 0.15s',
      fontFamily: 'inherit'
    }}>
              {o.label}
            </button>;
  })}
      </div>
    </div>;
  const [hwKey, setHwKey] = useState('H200');
  const [nGpus, setNGpus] = useState(8);
  const [modelKey, setModelKey] = useState('M1');
  const [contextLen, setContextLen] = useState(65536);
  const [sloS, setSloS] = useState(10);
  const [result, setResult] = useState(null);
  const [computing, setComputing] = useState(false);
  const [dark, setDark] = useState(false);
  const computeIdRef = useRef(0);
  useEffect(() => {
    if (typeof document === 'undefined') return;
    const root = document.documentElement;
    const apply = () => setDark(root.classList.contains('dark'));
    apply();
    const obs = new MutationObserver(apply);
    obs.observe(root, {
      attributes: true,
      attributeFilter: ['class']
    });
    return () => obs.disconnect();
  }, []);
  const t = dark ? {
    bg: '#0f1115',
    surface: '#161922',
    surfaceMuted: '#1c2030',
    border: 'rgba(255,255,255,0.08)',
    borderStrong: 'rgba(255,255,255,0.14)',
    text: '#f3f4f6',
    textMuted: '#9ca3af',
    textFaint: '#6b7280',
    accent: '#a5b4fc',
    accentDeep: '#7c8cff',
    accentBg: 'rgba(124, 140, 255, 0.12)',
    accentBorder: 'rgba(124, 140, 255, 0.30)',
    inputBg: '#1c2030',
    inputBorder: 'rgba(255,255,255,0.10)',
    segBg: '#1c2030',
    segActiveBg: '#262b3b',
    danger: '#f87171'
  } : {
    bg: '#ffffff',
    surface: '#ffffff',
    surfaceMuted: '#fafafa',
    border: 'rgba(0,0,0,0.07)',
    borderStrong: 'rgba(0,0,0,0.12)',
    text: '#111827',
    textMuted: '#6b7280',
    textFaint: '#9ca3af',
    accent: '#4137FF',
    accentDeep: '#3128cc',
    accentBg: 'rgba(65, 55, 255, 0.06)',
    accentBorder: 'rgba(65, 55, 255, 0.20)',
    inputBg: '#ffffff',
    inputBorder: 'rgba(0,0,0,0.10)',
    segBg: '#f3f4f6',
    segActiveBg: '#ffffff',
    danger: '#dc2626'
  };
  useEffect(() => {
    const id = ++computeIdRef.current;
    setComputing(true);
    setResult(null);
    const timer = setTimeout(() => {
      if (id !== computeIdRef.current) return;
      try {
        const hw = HARDWARE[hwKey];
        const model = MODELS[modelKey];
        const [wDtype, kvDtype] = getDtypes(hw, model);
        const {maxN: maxAgents, nLow, nHigh, tier, band} = findMaxConcurrentWithBand(model, hw, nGpus, contextLen, sloS, wDtype, kvDtype, SPEC_ACCEPTANCE);
        const seatsAt40 = Math.floor(maxAgents / 0.40);
        const seatsAt25 = Math.floor(maxAgents / 0.25);
        setResult({
          maxAgents,
          nLow,
          nHigh,
          band,
          seatsAt40,
          seatsAt25,
          wDtype,
          kvDtype,
          tier
        });
      } catch (e) {
        setResult({
          error: e.message
        });
      } finally {
        if (id === computeIdRef.current) setComputing(false);
      }
    }, 20);
    return () => clearTimeout(timer);
  }, [hwKey, nGpus, modelKey, contextLen, sloS]);
  const dtypeLabel = result ? `${result.wDtype.toUpperCase()} weights / ${result.kvDtype.toUpperCase()} KV` : '';
  return <div className="not-prose" style={{
    fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
    background: t.surface,
    border: `1px solid ${t.border}`,
    borderRadius: 14,
    overflow: 'hidden',
    maxWidth: 760,
    boxShadow: dark ? 'none' : '0 1px 2px rgba(0,0,0,0.04)',
    color: t.text
  }}>
      {}
      <div style={{
    padding: '18px 22px 14px',
    borderBottom: `1px solid ${t.border}`,
    background: t.surfaceMuted
  }}>
        <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: 10
  }}>
          <span style={{
    display: 'inline-block',
    width: 8,
    height: 8,
    borderRadius: 999,
    background: t.accent,
    boxShadow: `0 0 0 4px ${t.accentBg}`
  }} />
          <div style={{
    fontSize: 15,
    fontWeight: 600,
    color: t.text,
    letterSpacing: '-0.01em'
  }}>
            Capacity planner
          </div>
        </div>
        <div style={{
    fontSize: 13,
    color: t.textMuted,
    marginTop: 6,
    lineHeight: 1.5
  }}>
          Estimate concurrent agents and supported developer seats for a Laguna deployment, based on Poolside's analytical inference simulator calibrated against measured benchmarks.
        </div>
      </div>

      {}
      <div style={{
    padding: '20px 22px',
    display: 'flex',
    flexDirection: 'column',
    gap: 18
  }}>
        <div style={{
    display: 'grid',
    gridTemplateColumns: '1fr 1fr',
    gap: 14
  }}>
          {renderSelect({
    label: 'Hardware',
    value: hwKey,
    onChange: setHwKey,
    options: HW_OPTIONS
  })}
          {renderSelect({
    label: 'Model',
    value: modelKey,
    onChange: setModelKey,
    options: MODEL_OPTIONS
  })}
        </div>
        {renderSegmented({
    label: 'Number of GPUs',
    value: nGpus,
    onChange: v => setNGpus(Number(v)),
    options: GPU_COUNT_OPTIONS
  })}
        {renderSegmented({
    label: 'Average context per task',
    hint: 'How large the agent\'s context window grows by the end of a typical task. Use a higher value for longer tasks, larger codebases, or workflows that read many files. Use a lower value for short, focused tasks.',
    value: contextLen,
    onChange: v => setContextLen(Number(v)),
    options: CONTEXT_OPTIONS
  })}
        {renderSegmented({
    label: 'Step-latency SLO',
    hint: 'p50 latency target per agent turn',
    value: sloS,
    onChange: v => setSloS(Number(v)),
    options: SLO_OPTIONS
  })}
      </div>

      {}
      {(() => {
    const failed = !computing && result && !result.error && result.maxAgents === 0;
    const dangerBg = dark ? 'rgba(248, 113, 113, 0.10)' : 'rgba(220, 38, 38, 0.05)';
    const dangerBorder = dark ? 'rgba(248, 113, 113, 0.35)' : 'rgba(220, 38, 38, 0.25)';
    return <div style={{
      margin: '0 22px 22px',
      background: failed ? dangerBg : t.accentBg,
      border: `1px solid ${failed ? dangerBorder : t.accentBorder}`,
      borderRadius: 12,
      padding: '18px 20px',
      transition: 'background 0.2s, border-color 0.2s',
      minHeight: 112,
      display: 'flex',
      alignItems: 'center'
    }}>
            {computing && <div style={{
      color: t.textMuted,
      fontSize: 13,
      width: '100%',
      textAlign: 'center',
      display: 'inline-flex',
      alignItems: 'center',
      justifyContent: 'center',
      gap: 8
    }}>
                <span style={{
      width: 12,
      height: 12,
      borderRadius: 999,
      border: `2px solid ${t.accentBorder}`,
      borderTopColor: t.accent,
      animation: 'cc-spin 0.7s linear infinite',
      display: 'inline-block'
    }} />
                <style>{'@keyframes cc-spin{to{transform:rotate(360deg)}}'}</style>
                Simulating…
              </div>}
            {!computing && result && result.error && <div style={{
      color: t.danger,
      fontSize: 13
    }}>{result.error}</div>}
            {!computing && result && !result.error && result.maxAgents === 0 && <div style={{
      width: '100%',
      display: 'flex',
      gap: 14,
      alignItems: 'flex-start'
    }}>
                <div style={{
      flexShrink: 0,
      width: 32,
      height: 32,
      borderRadius: 999,
      background: dangerBorder,
      color: dark ? '#fecaca' : '#dc2626',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      fontSize: 18,
      fontWeight: 700
    }}>!</div>
                <div style={{
      flex: 1
    }}>
                  <div style={{
      fontSize: 14,
      fontWeight: 600,
      color: dark ? '#fecaca' : '#991b1b',
      marginBottom: 4
    }}>
                    This configuration won't serve a single agent.
                  </div>
                  <div style={{
      fontSize: 12.5,
      color: t.textMuted,
      lineHeight: 1.5
    }}>
                    Either the weights + KV cache exceed the per-GPU HBM budget, or p50 step latency at one session already exceeds the {sloS}s SLO. Try a larger GPU count, a smaller model, lower context, or a looser SLO.
                  </div>
                </div>
              </div>}
            {!computing && result && !result.error && result.maxAgents > 0 && (() => {
      const tier = TIER_INFO[result.tier] || TIER_INFO.uncalibrated;
      return <div style={{
        width: '100%'
      }}>
                  <div style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'flex-start',
        marginBottom: 14,
        gap: 12
      }}>
                    <div style={{
        fontSize: 11,
        color: t.textMuted,
        fontWeight: 600,
        textTransform: 'uppercase',
        letterSpacing: '0.06em'
      }}>
                      Capacity
                    </div>
                    <span title={tier.expl} style={{
        display: 'inline-flex',
        alignItems: 'center',
        gap: 6,
        background: tier.color,
        color: '#fff',
        fontSize: 10.5,
        fontWeight: 600,
        padding: '4px 10px',
        borderRadius: 999,
        cursor: 'help',
        letterSpacing: '0.02em'
      }}>
                      <span style={{
        width: 6,
        height: 6,
        borderRadius: 999,
        background: '#fff',
        opacity: 0.85
      }} />
                      <span>{tier.label}</span>
                      <span style={{
        opacity: 0.8,
        fontWeight: 500
      }}>·</span>
                      <span style={{
        opacity: 0.9,
        fontWeight: 500
      }}>{tier.band}</span>
                    </span>
                  </div>
                  <div style={{
        display: 'grid',
        gridTemplateColumns: '1fr 1fr 1fr',
        gap: 4
      }}>
                    <div style={{
        paddingRight: 12,
        borderRight: `1px solid ${t.accentBorder}`
      }}>
                      <div style={{
        fontSize: 10.5,
        color: t.textMuted,
        fontWeight: 600,
        textTransform: 'uppercase',
        letterSpacing: '0.06em'
      }}>
                        Concurrent agents
                      </div>
                      <div style={{
        fontSize: 30,
        fontWeight: 700,
        color: t.accent,
        lineHeight: 1.15,
        marginTop: 4,
        fontVariantNumeric: 'tabular-nums',
        letterSpacing: '-0.02em'
      }}>
                        {result.maxAgents}
                      </div>
                      <div style={{
        fontSize: 11,
        color: t.textFaint,
        marginTop: 2
      }}>
                        active model slots
                      </div>
                    </div>
                    <div style={{
        paddingLeft: 16,
        paddingRight: 12,
        borderRight: `1px solid ${t.accentBorder}`
      }}>
                      <div style={{
        fontSize: 10.5,
        color: t.textMuted,
        fontWeight: 600,
        textTransform: 'uppercase',
        letterSpacing: '0.06em'
      }}>
                        Seats at 40%
                      </div>
                      <div style={{
        fontSize: 30,
        fontWeight: 700,
        color: t.text,
        lineHeight: 1.15,
        marginTop: 4,
        fontVariantNumeric: 'tabular-nums',
        letterSpacing: '-0.02em'
      }}>
                        ~{result.seatsAt40}
                      </div>
                      <div style={{
        fontSize: 11,
        color: t.textFaint,
        marginTop: 2
      }}>
                        conservative default
                      </div>
                    </div>
                    <div style={{
        paddingLeft: 16
      }}>
                      <div style={{
        fontSize: 10.5,
        color: t.textMuted,
        fontWeight: 600,
        textTransform: 'uppercase',
        letterSpacing: '0.06em'
      }}>
                        Seats at 25%
                      </div>
                      <div style={{
        fontSize: 30,
        fontWeight: 700,
        color: t.text,
        lineHeight: 1.15,
        marginTop: 4,
        fontVariantNumeric: 'tabular-nums',
        letterSpacing: '-0.02em'
      }}>
                        ~{result.seatsAt25}
                      </div>
                      <div style={{
        fontSize: 11,
        color: t.textFaint,
        marginTop: 2
      }}>
                        light concurrency
                      </div>
                    </div>
                  </div>
                </div>;
    })()}
          </div>;
  })()}

      {}
      <div style={{
    borderTop: `1px solid ${t.border}`,
    padding: '12px 22px',
    background: t.surfaceMuted
  }}>
        <div style={{
    fontSize: 11,
    color: t.textFaint,
    lineHeight: 1.55
  }}>
          {dtypeLabel || 'FP8 weights / FP8 KV'} · continuous batching · 8K chunked prefill · speculative decoding{(() => {
    const hw = HARDWARE[hwKey];
    const bytes = hostKvOffloadBytes(hw, nGpus);
    if (bytes === 0) return ' · unified memory (no host KV offload)';
    return ` · ${Math.round(bytes / 1024 ** 3)} GB host KV offload`;
  })()}.
        </div>
      </div>
    </div>;
};

Use this page to estimate how many concurrent agents a Poolside deployment can support and how that capacity translates into developer seats. The planner models Laguna deployments across hardware, model, context, and latency assumptions.

For supported deployment paths and minimum hardware requirements, see [Supported configurations](/deployment/supported-configurations). The planner can model configurations that are useful for comparison, but it does not make an unsupported configuration supported.

<Note>
  Capacity estimates are planning inputs, not guarantees. Validate final sizing with your Poolside account team before you commit to production hardware or a large rollout.
</Note>

## Estimate capacity

Use the planner to estimate the maximum number of active agent tasks your deployment can sustain under the selected assumptions.

Set the inputs to match the deployment you are planning:

* **Hardware**: Select the GPU type you want to model.
* **Model**: Select the Laguna model you plan to deploy.
* **Number of GPUs**: Select the number of GPUs assigned to the model-serving node.
* **Average context per task**: Select how large the agent's context window grows by the end of a typical task. Use a higher value for longer tasks, larger codebases, or workflows that read many files. Use a lower value for short, focused tasks.
* **Step-latency SLO**: Select the p50 latency target per agent turn. A stricter SLO lowers the number of concurrent agents the deployment can serve.

The planner reports:

* **Concurrent agents**: The estimated number of active agent tasks that can occupy model-serving slots at the same time.
* **Seats at 40%**: A conservative developer-seat estimate for first-time sizing or agent-heavy usage.
* **Seats at 25%**: A lighter-concurrency estimate for pilots, mixed workloads, or deployments with telemetry that shows lower peak activity.

<CapacityCalculator />

If the planner reports that a configuration cannot serve a single agent, increase the GPU count, choose a smaller model, reduce the average context size, or relax the step-latency SLO.

## Interpret the estimate

Concurrent-agent capacity is not the same as the number of developers a deployment supports. A developer consumes a model-serving slot only while an agent task is actively running. Outside of an active task, the developer does not consume agent capacity.

To estimate supported seats, divide concurrent-agent capacity by the fraction of seats actively running an agent at peak:

```text title="Seat estimate" theme={null}
seats = concurrent-agent capacity / active-concurrency ratio
```

Use a planning range of **25 to 40 percent**, with **40 percent as the conservative default for initial sizing**.

Laguna agent tasks typically take two to three minutes to complete. Each active agent occupies a slot for that full duration, so the instantaneous concurrency ratio for agent workloads runs higher than for chat-style models. Without real-world telemetry from your deployment, plan against the higher end of the range.

Use 25 percent when:

* Your deployment has telemetry showing light concurrency
* Your workload mixes agent and chat usage
* You are sizing a pilot or limited rollout

Use 40 percent when:

* This is a first-time sizing without observed concurrency
* The team works in an agent-first culture
* Latency degradation is particularly disruptive in your environment

## Understand calibration confidence

The planner uses an analytical inference simulator calibrated against measured Poolside benchmarks. The confidence badge in the planner indicates how closely the selected configuration matches measured data:

* **Calibrated**: Direct measurement exists for the selected model, GPU, GPU count, and precision.
* **Same arch**: Measurement exists for the same model on the same GPU architecture.
* **Factorized**, **partial signal**, or **arch median**: The estimate depends more heavily on extrapolation.

Use extrapolated estimates for comparison and early planning. For production sizing, validate the selected configuration with Poolside against your expected workload.

## What affects capacity

Real-world capacity depends on your deployment shape and workload:

* Model choice
* GPU type and GPU count
* Weight and key-value cache precision
* Average context size per trajectory
* Step-latency target
* Number of steps each agent task takes
* Mix of agent and chat workloads
* Peak-time concurrency and burst behavior

Use the planner output as a conservative starting point. As your team builds usage history, replace the default planning assumptions with your own observed values.

## Choose a model for your deployment

For full model details, see [Supported models](/get-started/supported-models).

| Model       | When to choose it                                                                                                                                                                  |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Laguna XS.2 | Use when concurrent-agent throughput is the priority, you have limited GPU availability, or you need a strong default for most agent workloads.                                    |
| Laguna M.1  | Use when agent quality matters more than raw throughput. It is the best fit on 8× H200 hardware and can serve smaller teams on RTX 6000 Blackwell when you need lower concurrency. |

Malibu 2.2 remains available for existing deployments and dense-model preferences, but the planner focuses on Laguna capacity. For Malibu sizing, contact your Poolside account team.

## Scale beyond a single node

Both on-premises and cloud deployments support multi-node GPU inference within a single Kubernetes cluster. Multi-node configurations distribute independent inference replicas across nodes to add throughput. Cross-node tensor parallelism is not supported, and multi-node configurations do not provide high availability against node failures.

For deployment-specific configuration, see [Supported configurations](/deployment/supported-configurations) for on-premises, [Manage models on OpenShift](/deployment/cloud/openshift/manage-models), or [Manage models on upstream Kubernetes](/deployment/cloud/upstream-kubernetes/manage-models).

## Keep estimates current

Capacity changes as Poolside improves quantization formats, key-value cache handling, request scheduling, and inference-engine performance. Revisit this page when you change model versions, hardware, workload mix, or latency targets.

## Related resources

* [Supported configurations](/deployment/supported-configurations)
* [Supported models](/get-started/supported-models)
* [On-premises deployment](/deployment/on-prem/overview)
