Compare commits
3 Commits
db034687d6
...
19f652b43a
| Author | SHA1 | Date | |
|---|---|---|---|
| 19f652b43a | |||
| 57a81be769 | |||
| bbb69a315c |
@@ -162,7 +162,8 @@ function CohortStageBreakdown({ cohorts }: { cohorts: DeploymentCohort[] }) {
|
|||||||
|
|
||||||
function NetworkHealthIndicator({ dc }: { dc: DataCenter }) {
|
function NetworkHealthIndicator({ dc }: { dc: DataCenter }) {
|
||||||
const ns = dc.networkSummary;
|
const ns = dc.networkSummary;
|
||||||
if (ns.switchIds.length === 0) return null;
|
const torTotal = ns.totalByTier?.tor ?? 0;
|
||||||
|
if (torTotal === 0) return null;
|
||||||
|
|
||||||
const hasDisconnected = ns.racksDisconnected > 0;
|
const hasDisconnected = ns.racksDisconnected > 0;
|
||||||
const hasDegraded = ns.racksDegraded > 0;
|
const hasDegraded = ns.racksDegraded > 0;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Play, Rocket, Globe, ChevronDown, ChevronUp, Beaker, Shield, Zap, BarChart3 } from 'lucide-react';
|
import { Play, Rocket, Globe, ChevronDown, ChevronUp, Beaker, Shield, Zap } from 'lucide-react';
|
||||||
import { TutorialHint } from '@/components/game/TutorialHint';
|
import { TutorialHint } from '@/components/game/TutorialHint';
|
||||||
import { ConfirmModal } from '@/components/common/ConfirmModal';
|
import { ConfirmModal } from '@/components/common/ConfirmModal';
|
||||||
import { useGameStore } from '@/store';
|
import { useGameStore } from '@/store';
|
||||||
@@ -16,10 +16,9 @@ import {
|
|||||||
} from '@ai-tycoon/shared';
|
} from '@ai-tycoon/shared';
|
||||||
import type {
|
import type {
|
||||||
ModelArchitecture, DataMixAllocation, SFTSpecialization, AlignmentMethod,
|
ModelArchitecture, DataMixAllocation, SFTSpecialization, AlignmentMethod,
|
||||||
DataDomain, QuantizationLevel, BaseModel, ModelVariant, BenchmarkResult,
|
DataDomain, QuantizationLevel, BaseModel, ModelVariant,
|
||||||
SizeTier, ModelFamily,
|
SizeTier, ModelFamily,
|
||||||
} from '@ai-tycoon/shared';
|
} from '@ai-tycoon/shared';
|
||||||
import { BENCHMARKS } from '@ai-tycoon/game-engine';
|
|
||||||
|
|
||||||
const DATA_MIX_PRESETS: Record<string, { label: string; mix: DataMixAllocation }> = {
|
const DATA_MIX_PRESETS: Record<string, { label: string; mix: DataMixAllocation }> = {
|
||||||
balanced: { label: 'Balanced', mix: DEFAULT_DATA_MIX },
|
balanced: { label: 'Balanced', mix: DEFAULT_DATA_MIX },
|
||||||
@@ -52,8 +51,6 @@ export function ModelsPage() {
|
|||||||
const families = useGameStore((s) => s.models.families);
|
const families = useGameStore((s) => s.models.families);
|
||||||
const pipelines = useGameStore((s) => s.models.activeTrainingPipelines);
|
const pipelines = useGameStore((s) => s.models.activeTrainingPipelines);
|
||||||
const variantJobs = useGameStore((s) => s.models.variantJobs);
|
const variantJobs = useGameStore((s) => s.models.variantJobs);
|
||||||
const evalJobs = useGameStore((s) => s.models.evalJobs);
|
|
||||||
const benchmarkResults = useGameStore((s) => s.models.benchmarkResults);
|
|
||||||
const productLines = useGameStore((s) => s.models.productLines);
|
const productLines = useGameStore((s) => s.models.productLines);
|
||||||
const totalFlops = useGameStore((s) => s.compute.totalFlops);
|
const totalFlops = useGameStore((s) => s.compute.totalFlops);
|
||||||
const totalVramGB = useGameStore((s) => s.compute.totalVramGB);
|
const totalVramGB = useGameStore((s) => s.compute.totalVramGB);
|
||||||
@@ -64,7 +61,6 @@ export function ModelsPage() {
|
|||||||
const deployModel = useGameStore((s) => s.deployModel);
|
const deployModel = useGameStore((s) => s.deployModel);
|
||||||
const deployVariant = useGameStore((s) => s.deployVariant);
|
const deployVariant = useGameStore((s) => s.deployVariant);
|
||||||
const createQuantization = useGameStore((s) => s.createQuantization);
|
const createQuantization = useGameStore((s) => s.createQuantization);
|
||||||
const startEvaluation = useGameStore((s) => s.startEvaluation);
|
|
||||||
const setTrainingAllocation = useGameStore((s) => s.setTrainingAllocation);
|
const setTrainingAllocation = useGameStore((s) => s.setTrainingAllocation);
|
||||||
const openSourceModel = useGameStore((s) => s.openSourceModel);
|
const openSourceModel = useGameStore((s) => s.openSourceModel);
|
||||||
const openSourcedModels = useGameStore((s) => s.market.openSourcedModels);
|
const openSourcedModels = useGameStore((s) => s.market.openSourcedModels);
|
||||||
@@ -96,15 +92,12 @@ export function ModelsPage() {
|
|||||||
|
|
||||||
const activePipelines = pipelines.filter(p => p.status === 'active' || p.status === 'stalled');
|
const activePipelines = pipelines.filter(p => p.status === 'active' || p.status === 'stalled');
|
||||||
const activeVariantJobs = variantJobs.filter(j => j.status === 'active');
|
const activeVariantJobs = variantJobs.filter(j => j.status === 'active');
|
||||||
const activeEvalJobs = evalJobs.filter(j => j.status === 'active');
|
|
||||||
const undeployedCount = baseModels.filter(m => !m.isDeployed).length;
|
const undeployedCount = baseModels.filter(m => !m.isDeployed).length;
|
||||||
const hasActiveJobs = activePipelines.length > 0 || activeVariantJobs.length > 0 || activeEvalJobs.length > 0;
|
const hasActiveJobs = activePipelines.length > 0 || activeVariantJobs.length > 0;
|
||||||
const noModelDeployed = baseModels.length > 0 && !baseModels.some(m => m.isDeployed);
|
const noModelDeployed = baseModels.length > 0 && !baseModels.some(m => m.isDeployed);
|
||||||
|
|
||||||
const eraOrder = ['startup', 'scaleup', 'bigtech', 'agi'] as const;
|
const eraOrder = ['startup', 'scaleup', 'bigtech', 'agi'] as const;
|
||||||
const currentEraIdx = eraOrder.indexOf(currentEra);
|
const currentEraIdx = eraOrder.indexOf(currentEra);
|
||||||
const availableBenchmarks = BENCHMARKS.filter(b => eraOrder.indexOf(b.unlockedAtEra) <= currentEraIdx);
|
|
||||||
|
|
||||||
const hasAlignmentResearch = completedResearch.some(r =>
|
const hasAlignmentResearch = completedResearch.some(r =>
|
||||||
r === 'alignment-research' || r === 'interpretability' || r === 'constitutional-ai',
|
r === 'alignment-research' || r === 'interpretability' || r === 'constitutional-ai',
|
||||||
);
|
);
|
||||||
@@ -186,7 +179,6 @@ export function ModelsPage() {
|
|||||||
{ id: 'overview' as const, label: 'Overview' },
|
{ id: 'overview' as const, label: 'Overview' },
|
||||||
{ id: 'train' as const, label: 'Train New' },
|
{ id: 'train' as const, label: 'Train New' },
|
||||||
{ id: 'models' as const, label: `Families${families.length > 0 ? ` (${families.length})` : ''}` },
|
{ id: 'models' as const, label: `Families${families.length > 0 ? ` (${families.length})` : ''}` },
|
||||||
{ id: 'benchmarks' as const, label: 'Benchmarks' },
|
|
||||||
{ id: 'products' as const, label: 'Products' },
|
{ id: 'products' as const, label: 'Products' },
|
||||||
]).map(tab => (
|
]).map(tab => (
|
||||||
<button
|
<button
|
||||||
@@ -347,28 +339,6 @@ export function ModelsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Active Eval Jobs */}
|
|
||||||
{modelsTab === 'overview' && activeEvalJobs.length > 0 && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h3 className="font-semibold">Running Evaluations</h3>
|
|
||||||
{activeEvalJobs.map(job => {
|
|
||||||
const model = baseModels.find(m => m.id === job.modelId) ?? families.flatMap(f => f.variants).find(v => v.id === job.modelId);
|
|
||||||
const progress = job.progressTicks / job.totalTicks;
|
|
||||||
return (
|
|
||||||
<div key={job.id} className="bg-surface-900 border border-surface-700 rounded-xl p-3">
|
|
||||||
<div className="flex items-center justify-between mb-1">
|
|
||||||
<span className="text-sm">{model?.name ?? 'Unknown'} — {job.benchmarkIds.length} benchmarks</span>
|
|
||||||
<span className="text-xs text-surface-400">{formatPercent(progress)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="h-1.5 bg-surface-800 rounded-full overflow-hidden">
|
|
||||||
<div className="h-full bg-blue-500 rounded-full transition-all" style={{ width: `${progress * 100}%` }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Train New Model */}
|
{/* Train New Model */}
|
||||||
{modelsTab === 'train' && <div className="bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-4">
|
{modelsTab === 'train' && <div className="bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-4">
|
||||||
<h3 className="font-semibold">Train New Model</h3>
|
<h3 className="font-semibold">Train New Model</h3>
|
||||||
@@ -716,9 +686,8 @@ export function ModelsPage() {
|
|||||||
{familyModels.map(model => (
|
{familyModels.map(model => (
|
||||||
<div key={model.id} className="space-y-3">
|
<div key={model.id} className="space-y-3">
|
||||||
<h5 className="text-sm font-medium text-surface-300">{model.name}</h5>
|
<h5 className="text-sm font-medium text-surface-300">{model.name}</h5>
|
||||||
<ModelDetails model={model} benchmarkResults={benchmarkResults} />
|
<ModelDetails model={model} />
|
||||||
<QuantizationCreator model={model} completedResearch={completedResearch} onQuantize={createQuantization} />
|
<QuantizationCreator model={model} completedResearch={completedResearch} onQuantize={createQuantization} />
|
||||||
<BenchmarkEvaluator modelId={model.id} modelName={model.name} availableBenchmarks={availableBenchmarks} benchmarkResults={benchmarkResults} evalJobs={evalJobs} onStartEval={startEvaluation} />
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -730,11 +699,7 @@ export function ModelsPage() {
|
|||||||
key={variant.id}
|
key={variant.id}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
familyId={family.id}
|
familyId={family.id}
|
||||||
benchmarkResults={benchmarkResults}
|
|
||||||
availableBenchmarks={availableBenchmarks}
|
|
||||||
evalJobs={evalJobs}
|
|
||||||
onDeploy={() => deployVariant(family.id, variant.id)}
|
onDeploy={() => deployVariant(family.id, variant.id)}
|
||||||
onStartEval={startEvaluation}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -747,21 +712,6 @@ export function ModelsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Benchmark Leaderboard */}
|
|
||||||
{modelsTab === 'benchmarks' && benchmarkResults.length > 0 && (
|
|
||||||
<BenchmarkLeaderboard
|
|
||||||
benchmarkResults={benchmarkResults}
|
|
||||||
baseModels={baseModels}
|
|
||||||
families={families}
|
|
||||||
availableBenchmarks={availableBenchmarks}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{modelsTab === 'benchmarks' && benchmarkResults.length === 0 && (
|
|
||||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-8 text-center text-surface-500 text-sm">
|
|
||||||
No benchmark results yet. Run evaluations from the Models tab.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Product Lines */}
|
{/* Product Lines */}
|
||||||
{modelsTab === 'products' && <div className="space-y-3">
|
{modelsTab === 'products' && <div className="space-y-3">
|
||||||
<h3 className="font-semibold">Product Lines</h3>
|
<h3 className="font-semibold">Product Lines</h3>
|
||||||
@@ -865,9 +815,7 @@ function ModelActions({ model, isOpenSourced, onDeploy, onOpenSource }: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModelDetails({ model, benchmarkResults }: { model: BaseModel; benchmarkResults: BenchmarkResult[] }) {
|
function ModelDetails({ model }: { model: BaseModel }) {
|
||||||
const modelResults = benchmarkResults.filter(r => r.modelId === model.id);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="grid grid-cols-3 gap-3 text-xs">
|
<div className="grid grid-cols-3 gap-3 text-xs">
|
||||||
@@ -907,22 +855,6 @@ function ModelDetails({ model, benchmarkResults }: { model: BaseModel; benchmark
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{modelResults.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<span className="text-xs font-medium text-surface-300">Benchmark Scores</span>
|
|
||||||
<div className="grid grid-cols-3 gap-2 mt-1">
|
|
||||||
{modelResults.map(r => {
|
|
||||||
const bench = BENCHMARKS.find(b => b.id === r.benchmarkId);
|
|
||||||
return (
|
|
||||||
<div key={r.benchmarkId} className="bg-surface-800 rounded-lg p-2 text-xs">
|
|
||||||
<span className="text-surface-400">{bench?.name ?? r.benchmarkId}</span>
|
|
||||||
<div className="font-mono mt-0.5 text-accent-light">{r.score.toFixed(1)}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -981,91 +913,12 @@ function QuantizationCreator({ model, completedResearch, onQuantize }: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BenchmarkEvaluator({ modelId, modelName, availableBenchmarks, benchmarkResults, evalJobs, onStartEval }: {
|
function VariantCard({ variant, familyId, onDeploy }: {
|
||||||
modelId: string;
|
|
||||||
modelName: string;
|
|
||||||
availableBenchmarks: typeof BENCHMARKS;
|
|
||||||
benchmarkResults: BenchmarkResult[];
|
|
||||||
evalJobs: { id: string; modelId: string; status: string }[];
|
|
||||||
onStartEval: (modelId: string, benchmarkIds: string[]) => void;
|
|
||||||
}) {
|
|
||||||
const [showEval, setShowEval] = useState(false);
|
|
||||||
const [selectedBenchmarks, setSelectedBenchmarks] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const existingResults = benchmarkResults.filter(r => r.modelId === modelId);
|
|
||||||
const evaluatedIds = new Set(existingResults.map(r => r.benchmarkId));
|
|
||||||
const isEvaluating = evalJobs.some(j => j.modelId === modelId && j.status === 'active');
|
|
||||||
const unevaluated = availableBenchmarks.filter(b => !evaluatedIds.has(b.id));
|
|
||||||
|
|
||||||
if (unevaluated.length === 0 && !showEval) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!showEval) {
|
|
||||||
return (
|
|
||||||
<button onClick={() => { setShowEval(true); setSelectedBenchmarks(unevaluated.map(b => b.id)); }}
|
|
||||||
disabled={isEvaluating}
|
|
||||||
className="flex items-center gap-1 text-xs text-blue-400 hover:text-blue-300 disabled:opacity-50">
|
|
||||||
<BarChart3 size={12} /> Run Benchmarks ({unevaluated.length} available)
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-surface-800/50 rounded-lg p-3 space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs font-medium text-surface-300">Run Evaluation</span>
|
|
||||||
<button onClick={() => setShowEval(false)} className="text-xs text-surface-500 hover:text-surface-300">Close</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{availableBenchmarks.map(bench => {
|
|
||||||
const alreadyDone = evaluatedIds.has(bench.id);
|
|
||||||
const selected = selectedBenchmarks.includes(bench.id);
|
|
||||||
return (
|
|
||||||
<button key={bench.id}
|
|
||||||
disabled={alreadyDone}
|
|
||||||
onClick={() => setSelectedBenchmarks(prev =>
|
|
||||||
prev.includes(bench.id) ? prev.filter(id => id !== bench.id) : [...prev, bench.id]
|
|
||||||
)}
|
|
||||||
className={`px-2 py-0.5 rounded text-[10px] border ${
|
|
||||||
alreadyDone ? 'bg-success/10 border-success/30 text-success cursor-default' :
|
|
||||||
selected ? 'bg-blue-500/20 border-blue-500 text-blue-300' :
|
|
||||||
'bg-surface-800 border-surface-600 text-surface-400'
|
|
||||||
}`}
|
|
||||||
title={bench.description}
|
|
||||||
>
|
|
||||||
{bench.name} {alreadyDone ? `(${existingResults.find(r => r.benchmarkId === bench.id)?.score.toFixed(0)})` : ''}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{selectedBenchmarks.length > 0 && (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-[10px] text-surface-500">
|
|
||||||
{selectedBenchmarks.length} benchmark{selectedBenchmarks.length > 1 ? 's' : ''} · ~{availableBenchmarks.filter(b => selectedBenchmarks.includes(b.id)).reduce((s, b) => s + b.ticksToRun, 0)} ticks
|
|
||||||
</span>
|
|
||||||
<button onClick={() => { onStartEval(modelId, selectedBenchmarks); setShowEval(false); }}
|
|
||||||
disabled={isEvaluating}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white rounded px-3 py-1 text-xs disabled:opacity-50">
|
|
||||||
Evaluate
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function VariantCard({ variant, familyId, benchmarkResults, availableBenchmarks, evalJobs, onDeploy, onStartEval }: {
|
|
||||||
variant: ModelVariant;
|
variant: ModelVariant;
|
||||||
familyId: string;
|
familyId: string;
|
||||||
benchmarkResults: BenchmarkResult[];
|
|
||||||
availableBenchmarks: typeof BENCHMARKS;
|
|
||||||
evalJobs: { id: string; modelId: string; status: string }[];
|
|
||||||
onDeploy: () => void;
|
onDeploy: () => void;
|
||||||
onStartEval: (modelId: string, benchmarkIds: string[]) => void;
|
|
||||||
}) {
|
}) {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const variantResults = benchmarkResults.filter(r => r.modelId === variant.id);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-surface-800/50 rounded-lg p-3 ml-4 border-l-2 border-surface-600">
|
<div className="bg-surface-800/50 rounded-lg p-3 ml-4 border-l-2 border-surface-600">
|
||||||
@@ -1106,108 +959,12 @@ function VariantCard({ variant, familyId, benchmarkResults, availableBenchmarks,
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{variantResults.length > 0 && (
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
{variantResults.map(r => {
|
|
||||||
const bench = BENCHMARKS.find(b => b.id === r.benchmarkId);
|
|
||||||
return (
|
|
||||||
<div key={r.benchmarkId} className="bg-surface-800 rounded p-1.5 text-xs">
|
|
||||||
<span className="text-surface-400 text-[10px]">{bench?.name ?? r.benchmarkId}</span>
|
|
||||||
<div className="font-mono text-accent-light text-[11px]">{r.score.toFixed(1)}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<BenchmarkEvaluator
|
|
||||||
modelId={variant.id}
|
|
||||||
modelName={variant.name}
|
|
||||||
availableBenchmarks={availableBenchmarks}
|
|
||||||
benchmarkResults={benchmarkResults}
|
|
||||||
evalJobs={evalJobs}
|
|
||||||
onStartEval={onStartEval}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BenchmarkLeaderboard({ benchmarkResults, baseModels, families, availableBenchmarks }: {
|
|
||||||
benchmarkResults: BenchmarkResult[];
|
|
||||||
baseModels: BaseModel[];
|
|
||||||
families: { id: string; name: string; variants: ModelVariant[] }[];
|
|
||||||
availableBenchmarks: typeof BENCHMARKS;
|
|
||||||
}) {
|
|
||||||
const allModels: (BaseModel | ModelVariant)[] = [
|
|
||||||
...baseModels,
|
|
||||||
...families.flatMap(f => f.variants),
|
|
||||||
];
|
|
||||||
|
|
||||||
const modelNames = new Map(allModels.map(m => [m.id, m.name]));
|
|
||||||
const benchmarksWithResults = availableBenchmarks.filter(b =>
|
|
||||||
benchmarkResults.some(r => r.benchmarkId === b.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (benchmarksWithResults.length === 0) return null;
|
|
||||||
|
|
||||||
const modelIds = [...new Set(benchmarkResults.map(r => r.modelId))];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
|
||||||
<h3 className="font-semibold mb-3 flex items-center gap-2">
|
|
||||||
<BarChart3 size={16} /> Benchmark Leaderboard
|
|
||||||
</h3>
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full text-xs">
|
|
||||||
<thead>
|
|
||||||
<tr className="border-b border-surface-700">
|
|
||||||
<th className="text-left py-1.5 pr-3 text-surface-400 font-medium">Model</th>
|
|
||||||
{benchmarksWithResults.map(b => (
|
|
||||||
<th key={b.id} className="text-center py-1.5 px-2 text-surface-400 font-medium">{b.name}</th>
|
|
||||||
))}
|
|
||||||
<th className="text-center py-1.5 px-2 text-surface-400 font-medium">Avg</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{modelIds.map(modelId => {
|
|
||||||
const results = benchmarkResults.filter(r => r.modelId === modelId);
|
|
||||||
const scores = benchmarksWithResults.map(b => {
|
|
||||||
const r = results.find(r => r.benchmarkId === b.id);
|
|
||||||
return r?.score ?? null;
|
|
||||||
});
|
|
||||||
const validScores = scores.filter((s): s is number => s !== null);
|
|
||||||
const avg = validScores.length > 0 ? validScores.reduce((a, b) => a + b, 0) / validScores.length : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr key={modelId} className="border-b border-surface-800">
|
|
||||||
<td className="py-1.5 pr-3 font-medium">{modelNames.get(modelId) ?? 'Unknown'}</td>
|
|
||||||
{scores.map((score, i) => (
|
|
||||||
<td key={i} className="text-center py-1.5 px-2 font-mono">
|
|
||||||
{score !== null ? (
|
|
||||||
<span className={score >= 80 ? 'text-success' : score >= 50 ? 'text-accent-light' : 'text-surface-400'}>
|
|
||||||
{score.toFixed(1)}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-surface-600">—</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
<td className="text-center py-1.5 px-2 font-mono font-medium text-accent-light">
|
|
||||||
{avg > 0 ? avg.toFixed(1) : '—'}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StageBar({ label, active, complete, progress }: {
|
function StageBar({ label, active, complete, progress }: {
|
||||||
label: string; active: boolean; complete: boolean; progress: number;
|
label: string; active: boolean; complete: boolean; progress: number;
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import type {
|
|||||||
TrainingPipeline, ModelFamily, DataMixAllocation,
|
TrainingPipeline, ModelFamily, DataMixAllocation,
|
||||||
ModelArchitecture, AlignmentMethod, SizeTier,
|
ModelArchitecture, AlignmentMethod, SizeTier,
|
||||||
SFTSpecialization, QuantizationLevel, VariantCreationJob,
|
SFTSpecialization, QuantizationLevel, VariantCreationJob,
|
||||||
EvalJob,
|
|
||||||
ConsumerTierId, ApiTierId,
|
ConsumerTierId, ApiTierId,
|
||||||
} from '@ai-tycoon/shared';
|
} from '@ai-tycoon/shared';
|
||||||
import {
|
import {
|
||||||
@@ -43,7 +42,7 @@ import {
|
|||||||
} from '@ai-tycoon/shared';
|
} from '@ai-tycoon/shared';
|
||||||
import {
|
import {
|
||||||
emptyDCNetworkSummary, emptyCampusNetworkSummary, emptyClusterNetworkSummary,
|
emptyDCNetworkSummary, emptyCampusNetworkSummary, emptyClusterNetworkSummary,
|
||||||
BENCHMARKS, TECH_TREE, onModelDeployed,
|
TECH_TREE, onModelDeployed,
|
||||||
} from '@ai-tycoon/game-engine';
|
} from '@ai-tycoon/game-engine';
|
||||||
import { INITIAL_RIVALS } from '@ai-tycoon/game-engine';
|
import { INITIAL_RIVALS } from '@ai-tycoon/game-engine';
|
||||||
|
|
||||||
@@ -59,7 +58,7 @@ export interface InfraNav {
|
|||||||
datacenterId?: string;
|
datacenterId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModelsTab = 'overview' | 'train' | 'models' | 'benchmarks' | 'products';
|
type ModelsTab = 'overview' | 'train' | 'models' | 'products';
|
||||||
|
|
||||||
interface UIState {
|
interface UIState {
|
||||||
activePage: ActivePage;
|
activePage: ActivePage;
|
||||||
@@ -132,7 +131,6 @@ interface Actions {
|
|||||||
}) => void;
|
}) => void;
|
||||||
startPointRelease: (baseModelId: string) => void;
|
startPointRelease: (baseModelId: string) => void;
|
||||||
createQuantization: (baseModelId: string, level: QuantizationLevel, variantName: string) => void;
|
createQuantization: (baseModelId: string, level: QuantizationLevel, variantName: string) => void;
|
||||||
startEvaluation: (modelId: string, benchmarkIds: string[]) => void;
|
|
||||||
deployModel: (modelId: string) => void;
|
deployModel: (modelId: string) => void;
|
||||||
deployVariant: (familyId: string, variantId: string) => void;
|
deployVariant: (familyId: string, variantId: string) => void;
|
||||||
setProductPricing: (productLineId: string, field: string, value: number) => void;
|
setProductPricing: (productLineId: string, field: string, value: number) => void;
|
||||||
@@ -1076,32 +1074,6 @@ export const useGameStore = create<Store>()(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
startEvaluation: (modelId, benchmarkIds) => {
|
|
||||||
let created = false;
|
|
||||||
set((s) => {
|
|
||||||
const benchmarks = BENCHMARKS.filter(b => benchmarkIds.includes(b.id));
|
|
||||||
if (benchmarks.length === 0) return s;
|
|
||||||
created = true;
|
|
||||||
const totalTicks = benchmarks.reduce((sum, b) => sum + b.ticksToRun, 0);
|
|
||||||
const computeCost = benchmarks.reduce((sum, b) => sum + b.computeCost, 0);
|
|
||||||
const job: EvalJob = {
|
|
||||||
id: uuid(),
|
|
||||||
modelId,
|
|
||||||
benchmarkIds,
|
|
||||||
progressTicks: 0,
|
|
||||||
totalTicks,
|
|
||||||
computeAllocated: computeCost,
|
|
||||||
status: 'active',
|
|
||||||
results: [],
|
|
||||||
};
|
|
||||||
return { models: { ...s.models, evalJobs: [...s.models.evalJobs, job] } };
|
|
||||||
});
|
|
||||||
if (created) {
|
|
||||||
get().addNotification({ title: 'Evaluation Started', message: `${benchmarkIds.length} benchmark${benchmarkIds.length > 1 ? 's' : ''} queued.`, type: 'info', tick: get().meta.tickCount });
|
|
||||||
set({ modelsTab: 'overview' as ModelsTab });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
deployModel: (modelId) => {
|
deployModel: (modelId) => {
|
||||||
const modelName = get().models.baseModels.find(m => m.id === modelId)?.name ?? 'Model';
|
const modelName = get().models.baseModels.find(m => m.id === modelId)?.name ?? 'Model';
|
||||||
set((s) => ({
|
set((s) => ({
|
||||||
@@ -1113,6 +1085,7 @@ export const useGameStore = create<Store>()(
|
|||||||
productLines: s.models.productLines.map(pl => ({
|
productLines: s.models.productLines.map(pl => ({
|
||||||
...pl, modelId, isActive: true,
|
...pl, modelId, isActive: true,
|
||||||
})),
|
})),
|
||||||
|
deploymentVersion: s.models.deploymentVersion + 1,
|
||||||
},
|
},
|
||||||
market: {
|
market: {
|
||||||
...s.market,
|
...s.market,
|
||||||
@@ -1132,6 +1105,7 @@ export const useGameStore = create<Store>()(
|
|||||||
? { ...f, variants: f.variants.map(v => v.id === variantId ? { ...v, isDeployed: true } : v) }
|
? { ...f, variants: f.variants.map(v => v.id === variantId ? { ...v, isDeployed: true } : v) }
|
||||||
: f,
|
: f,
|
||||||
),
|
),
|
||||||
|
deploymentVersion: s.models.deploymentVersion + 1,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
get().addNotification({ title: 'Variant Deployed', message: 'Variant is now live.', type: 'success', tick: get().meta.tickCount });
|
get().addNotification({ title: 'Variant Deployed', message: 'Variant is now live.', type: 'success', tick: get().meta.tickCount });
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import type { DeepPartial } from './createTestState';
|
|||||||
|
|
||||||
function emptyDCNetwork(): DCNetworkSummary {
|
function emptyDCNetwork(): DCNetworkSummary {
|
||||||
return {
|
return {
|
||||||
switchIds: [],
|
|
||||||
networkRackCount: 0,
|
|
||||||
totalByTier: {},
|
totalByTier: {},
|
||||||
healthyByTier: {},
|
healthyByTier: {},
|
||||||
|
repairBatches: [],
|
||||||
|
networkRackCount: 0,
|
||||||
racksDisconnected: 0,
|
racksDisconnected: 0,
|
||||||
racksDegraded: 0,
|
racksDegraded: 0,
|
||||||
averageBandwidth: 1,
|
averageBandwidth: 1,
|
||||||
@@ -20,11 +20,11 @@ function emptyDCNetwork(): DCNetworkSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function emptyCampusNetwork(): CampusNetworkSummary {
|
function emptyCampusNetwork(): CampusNetworkSummary {
|
||||||
return { switchIds: [], totalT4: 0, healthyT4: 0, crossDCBandwidth: 1 };
|
return { totalT4: 0, healthyT4: 0, crossDCBandwidth: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
function emptyClusterNetwork(): ClusterNetworkSummary {
|
function emptyClusterNetwork(): ClusterNetworkSummary {
|
||||||
return { switchIds: [], totalT5: 0, healthyT5: 0, crossCampusBandwidth: 1 };
|
return { totalT5: 0, healthyT5: 0, crossCampusBandwidth: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createTestDataCenter(overrides?: DeepPartial<DataCenter>): DataCenter {
|
export function createTestDataCenter(overrides?: DeepPartial<DataCenter>): DataCenter {
|
||||||
@@ -171,7 +171,6 @@ export function createTestBaseModel(overrides?: Partial<BaseModel>): BaseModel {
|
|||||||
sizeTier: 'small',
|
sizeTier: 'small',
|
||||||
isPointRelease: false,
|
isPointRelease: false,
|
||||||
sourceModelId: null,
|
sourceModelId: null,
|
||||||
benchmarkResults: {},
|
|
||||||
dataMix: { web: 0.4, code: 0.2, books: 0.15, academic: 0.1, conversational: 0.1, specialized: 0.05 },
|
dataMix: { web: 0.4, code: 0.2, books: 0.15, academic: 0.1, conversational: 0.1, specialized: 0.05 },
|
||||||
};
|
};
|
||||||
return overrides ? { ...base, ...overrides } : base;
|
return overrides ? { ...base, ...overrides } : base;
|
||||||
@@ -181,9 +180,10 @@ export function createTestModelFamily(overrides?: Partial<ModelFamily>): ModelFa
|
|||||||
const base: ModelFamily = {
|
const base: ModelFamily = {
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
name: 'Test Family',
|
name: 'Test Family',
|
||||||
baseModels: [],
|
generation: 1,
|
||||||
|
baseModelIds: [],
|
||||||
variants: [],
|
variants: [],
|
||||||
activeEvals: [],
|
createdAtTick: 0,
|
||||||
};
|
};
|
||||||
return overrides ? { ...base, ...overrides } : base;
|
return overrides ? { ...base, ...overrides } : base;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
import type { BenchmarkDefinition } from '@ai-tycoon/shared';
|
|
||||||
|
|
||||||
export const BENCHMARKS: BenchmarkDefinition[] = [
|
|
||||||
{
|
|
||||||
id: 'arc-challenge',
|
|
||||||
name: 'ARC Challenge',
|
|
||||||
category: 'reasoning',
|
|
||||||
description: 'Advanced reasoning and comprehension tasks requiring multi-step inference.',
|
|
||||||
primaryCapability: 'reasoning',
|
|
||||||
secondaryCapability: 'knowledge',
|
|
||||||
computeCost: 0.001,
|
|
||||||
ticksToRun: 8,
|
|
||||||
unlockedAtEra: 'startup',
|
|
||||||
marketRelevance: { consumer: 0.3, enterprise: 0.5, developer: 0.4, research: 0.8 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'codeforce',
|
|
||||||
name: 'CodeForce',
|
|
||||||
category: 'coding',
|
|
||||||
description: 'Competitive programming and software engineering benchmarks.',
|
|
||||||
primaryCapability: 'coding',
|
|
||||||
secondaryCapability: 'reasoning',
|
|
||||||
computeCost: 0.001,
|
|
||||||
ticksToRun: 8,
|
|
||||||
unlockedAtEra: 'startup',
|
|
||||||
marketRelevance: { consumer: 0.2, enterprise: 0.7, developer: 0.9, research: 0.5 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mathquest',
|
|
||||||
name: 'MathQuest',
|
|
||||||
category: 'math',
|
|
||||||
description: 'Mathematical problem-solving from algebra to graduate-level proofs.',
|
|
||||||
primaryCapability: 'math',
|
|
||||||
secondaryCapability: 'reasoning',
|
|
||||||
computeCost: 0.001,
|
|
||||||
ticksToRun: 8,
|
|
||||||
unlockedAtEra: 'startup',
|
|
||||||
marketRelevance: { consumer: 0.1, enterprise: 0.6, developer: 0.5, research: 0.9 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'worldfacts',
|
|
||||||
name: 'WorldFacts',
|
|
||||||
category: 'knowledge',
|
|
||||||
description: 'Broad factual knowledge across science, history, culture, and current events.',
|
|
||||||
primaryCapability: 'knowledge',
|
|
||||||
secondaryCapability: 'reasoning',
|
|
||||||
computeCost: 0.001,
|
|
||||||
ticksToRun: 6,
|
|
||||||
unlockedAtEra: 'startup',
|
|
||||||
marketRelevance: { consumer: 0.5, enterprise: 0.4, developer: 0.3, research: 0.6 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'chatrank',
|
|
||||||
name: 'ChatRank',
|
|
||||||
category: 'chat',
|
|
||||||
description: 'Human preference evaluation of conversational quality, helpfulness, and creativity.',
|
|
||||||
primaryCapability: 'creative',
|
|
||||||
secondaryCapability: 'knowledge',
|
|
||||||
computeCost: 0.002,
|
|
||||||
ticksToRun: 10,
|
|
||||||
unlockedAtEra: 'startup',
|
|
||||||
marketRelevance: { consumer: 0.9, enterprise: 0.3, developer: 0.2, research: 0.2 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'harmguard',
|
|
||||||
name: 'HarmGuard',
|
|
||||||
category: 'safety',
|
|
||||||
description: 'Safety evaluation measuring harm avoidance, truthfulness, and responsible behavior.',
|
|
||||||
primaryCapability: 'reasoning',
|
|
||||||
computeCost: 0.001,
|
|
||||||
ticksToRun: 8,
|
|
||||||
unlockedAtEra: 'startup',
|
|
||||||
marketRelevance: { consumer: 0.4, enterprise: 0.9, developer: 0.3, research: 0.7 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'visionbench',
|
|
||||||
name: 'VisionBench',
|
|
||||||
category: 'multimodal',
|
|
||||||
description: 'Image understanding, visual reasoning, and multimodal comprehension.',
|
|
||||||
primaryCapability: 'multimodal',
|
|
||||||
secondaryCapability: 'reasoning',
|
|
||||||
computeCost: 0.003,
|
|
||||||
ticksToRun: 12,
|
|
||||||
unlockedAtEra: 'scaleup',
|
|
||||||
marketRelevance: { consumer: 0.5, enterprise: 0.6, developer: 0.6, research: 0.7 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'agentarena',
|
|
||||||
name: 'AgentArena',
|
|
||||||
category: 'agents',
|
|
||||||
description: 'Autonomous agent tasks: tool use, multi-step planning, and environment interaction.',
|
|
||||||
primaryCapability: 'agents',
|
|
||||||
secondaryCapability: 'coding',
|
|
||||||
computeCost: 0.005,
|
|
||||||
ticksToRun: 15,
|
|
||||||
unlockedAtEra: 'bigtech',
|
|
||||||
marketRelevance: { consumer: 0.3, enterprise: 0.8, developer: 0.7, research: 0.6 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'frontier-eval',
|
|
||||||
name: 'Frontier Eval',
|
|
||||||
category: 'reasoning',
|
|
||||||
description: 'Cutting-edge capability evaluation at the frontier of AI research.',
|
|
||||||
primaryCapability: 'reasoning',
|
|
||||||
secondaryCapability: 'math',
|
|
||||||
computeCost: 0.01,
|
|
||||||
ticksToRun: 20,
|
|
||||||
unlockedAtEra: 'agi',
|
|
||||||
marketRelevance: { consumer: 0.2, enterprise: 0.5, developer: 0.5, research: 1.0 },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -3,6 +3,7 @@ export { processTick, setAchievementDefinitions } from './tick';
|
|||||||
export type { TickNotification } from './tick';
|
export type { TickNotification } from './tick';
|
||||||
export { getAvailableResearch, getResearchNode } from './systems/researchSystem';
|
export { getAvailableResearch, getResearchNode } from './systems/researchSystem';
|
||||||
export { getResearchBonuses, resetResearchBonusCache } from './systems/researchBonuses';
|
export { getResearchBonuses, resetResearchBonusCache } from './systems/researchBonuses';
|
||||||
|
export { resetFleetCache } from './systems/market/servingPipeline';
|
||||||
export type { ResearchBonuses } from './systems/researchBonuses';
|
export type { ResearchBonuses } from './systems/researchBonuses';
|
||||||
export { emptyDCNetworkSummary, emptyCampusNetworkSummary, emptyClusterNetworkSummary } from './systems/infrastructureSystem';
|
export { emptyDCNetworkSummary, emptyCampusNetworkSummary, emptyClusterNetworkSummary } from './systems/infrastructureSystem';
|
||||||
export { onModelDeployed } from './systems/market/obsolescenceSystem';
|
export { onModelDeployed } from './systems/market/obsolescenceSystem';
|
||||||
@@ -11,4 +12,3 @@ export { TECH_TREE } from './data/techTree';
|
|||||||
export { INITIAL_RIVALS } from './data/competitors';
|
export { INITIAL_RIVALS } from './data/competitors';
|
||||||
export { KEY_HIRE_POOL } from './data/keyHires';
|
export { KEY_HIRE_POOL } from './data/keyHires';
|
||||||
export { ACHIEVEMENT_DEFINITIONS } from './data/achievements';
|
export { ACHIEVEMENT_DEFINITIONS } from './data/achievements';
|
||||||
export { BENCHMARKS } from './data/benchmarks';
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type {
|
import type {
|
||||||
GameState, InfrastructureState, Cluster, Campus, DataCenter,
|
GameState, InfrastructureState, Cluster, Campus, DataCenter,
|
||||||
DeploymentCohort, PipelineStage, RackSkuId, NetworkSwitch,
|
DeploymentCohort, PipelineStage, RackSkuId,
|
||||||
SwitchTier, DCNetworkSummary, CampusNetworkSummary, ClusterNetworkSummary,
|
SwitchTier, DCNetworkSummary, CampusNetworkSummary, ClusterNetworkSummary,
|
||||||
CampusRetrofitQueue, DCTier, IntraNodeInterconnect, NetworkFabric, RackSkuConfig,
|
RepairBatch, CampusRetrofitQueue, DCTier, IntraNodeInterconnect, NetworkFabric, RackSkuConfig,
|
||||||
} from '@ai-tycoon/shared';
|
} from '@ai-tycoon/shared';
|
||||||
import {
|
import {
|
||||||
LOCATION_CONFIGS,
|
LOCATION_CONFIGS,
|
||||||
@@ -83,357 +83,202 @@ function binomialSample(n: number, p: number): number {
|
|||||||
return base + (Math.random() < frac ? 1 : 0);
|
return base + (Math.random() < frac ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Network Topology Construction ---
|
// --- Aggregate Network Model ---
|
||||||
|
|
||||||
let switchIdCounter = 0;
|
const DC_TIERS: SwitchTier[] = ['tor', 't1', 't2', 't3'];
|
||||||
|
|
||||||
function createSwitch(
|
|
||||||
tier: SwitchTier,
|
|
||||||
dcId: string | null,
|
|
||||||
campusId: string | null,
|
|
||||||
clusterId: string | null,
|
|
||||||
): NetworkSwitch {
|
|
||||||
const config = SWITCH_TIER_CONFIGS[tier];
|
|
||||||
return {
|
|
||||||
id: `${tier}-${dcId ?? campusId ?? clusterId ?? 'x'}-${switchIdCounter++}`,
|
|
||||||
tier,
|
|
||||||
status: 'healthy',
|
|
||||||
dcId, campusId, clusterId,
|
|
||||||
uplinkIds: [],
|
|
||||||
downlinkIds: [],
|
|
||||||
activeUplinks: config.uplinkCount,
|
|
||||||
totalUplinks: config.uplinkCount,
|
|
||||||
effectiveBandwidth: 1.0,
|
|
||||||
repairProgress: 0,
|
|
||||||
repairTotal: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function wireUplinks(child: NetworkSwitch, parents: NetworkSwitch[], count: number): void {
|
|
||||||
if (parents.length === 0) return;
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const parent = parents[i % parents.length];
|
|
||||||
child.uplinkIds.push(parent.id);
|
|
||||||
if (!parent.downlinkIds.includes(child.id)) {
|
|
||||||
parent.downlinkIds.push(child.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
child.activeUplinks = count;
|
|
||||||
child.effectiveBandwidth = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function emptyDCNetworkSummary(): DCNetworkSummary {
|
export function emptyDCNetworkSummary(): DCNetworkSummary {
|
||||||
return {
|
return {
|
||||||
switchIds: [], networkRackCount: 0,
|
|
||||||
totalByTier: {}, healthyByTier: {},
|
totalByTier: {}, healthyByTier: {},
|
||||||
|
repairBatches: [], networkRackCount: 0,
|
||||||
racksDisconnected: 0, racksDegraded: 0,
|
racksDisconnected: 0, racksDegraded: 0,
|
||||||
averageBandwidth: 1, effectiveFlopsFraction: 1,
|
averageBandwidth: 1, effectiveFlopsFraction: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emptyCampusNetworkSummary(): CampusNetworkSummary {
|
export function emptyCampusNetworkSummary(): CampusNetworkSummary {
|
||||||
return { switchIds: [], totalT4: 0, healthyT4: 0, crossDCBandwidth: 1 };
|
return { totalT4: 0, healthyT4: 0, crossDCBandwidth: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emptyClusterNetworkSummary(): ClusterNetworkSummary {
|
export function emptyClusterNetworkSummary(): ClusterNetworkSummary {
|
||||||
return { switchIds: [], totalT5: 0, healthyT5: 0, crossCampusBandwidth: 1 };
|
return { totalT5: 0, healthyT5: 0, crossCampusBandwidth: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildDCTopology(
|
function computeTopologyCounts(
|
||||||
computeRackCount: number,
|
computeRackCount: number,
|
||||||
dcTier: DCTier,
|
dcTier: DCTier,
|
||||||
dcId: string,
|
): Partial<Record<SwitchTier, number>> {
|
||||||
registry: Record<string, NetworkSwitch>,
|
if (computeRackCount <= 0) return {};
|
||||||
): DCNetworkSummary {
|
|
||||||
if (computeRackCount <= 0) return emptyDCNetworkSummary();
|
|
||||||
|
|
||||||
const switchIds: string[] = [];
|
|
||||||
|
|
||||||
const t3Count = T3_COUNT_PER_DC_TIER[dcTier];
|
|
||||||
const t3s: NetworkSwitch[] = [];
|
|
||||||
for (let i = 0; i < t3Count; i++) {
|
|
||||||
const sw = createSwitch('t3', dcId, null, null);
|
|
||||||
sw.totalUplinks = 0;
|
|
||||||
sw.activeUplinks = 0;
|
|
||||||
t3s.push(sw);
|
|
||||||
registry[sw.id] = sw;
|
|
||||||
switchIds.push(sw.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const t1Count = Math.ceil(computeRackCount / SWITCH_TIER_CONFIGS.t1.fanOut);
|
const t1Count = Math.ceil(computeRackCount / SWITCH_TIER_CONFIGS.t1.fanOut);
|
||||||
const t2Count = Math.ceil(t1Count / SWITCH_TIER_CONFIGS.t2.fanOut);
|
const t2Count = Math.ceil(t1Count / SWITCH_TIER_CONFIGS.t2.fanOut);
|
||||||
|
const t3Count = T3_COUNT_PER_DC_TIER[dcTier];
|
||||||
const t2s: NetworkSwitch[] = [];
|
return { tor: computeRackCount, t1: t1Count, t2: t2Count, t3: t3Count };
|
||||||
for (let i = 0; i < t2Count; i++) {
|
|
||||||
const sw = createSwitch('t2', dcId, null, null);
|
|
||||||
wireUplinks(sw, t3s, SWITCH_TIER_CONFIGS.t2.uplinkCount);
|
|
||||||
t2s.push(sw);
|
|
||||||
registry[sw.id] = sw;
|
|
||||||
switchIds.push(sw.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const t1s: NetworkSwitch[] = [];
|
|
||||||
for (let i = 0; i < t1Count; i++) {
|
|
||||||
const sw = createSwitch('t1', dcId, null, null);
|
|
||||||
wireUplinks(sw, t2s, SWITCH_TIER_CONFIGS.t1.uplinkCount);
|
|
||||||
t1s.push(sw);
|
|
||||||
registry[sw.id] = sw;
|
|
||||||
switchIds.push(sw.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < computeRackCount; i++) {
|
|
||||||
const sw = createSwitch('tor', dcId, null, null);
|
|
||||||
const primary = t1s[Math.floor(i / SWITCH_TIER_CONFIGS.t1.fanOut)];
|
|
||||||
const altIdx = (Math.floor(i / SWITCH_TIER_CONFIGS.t1.fanOut) + 1) % t1s.length;
|
|
||||||
const alt = t1s[altIdx];
|
|
||||||
if (t1s.length >= 2 && primary !== alt) {
|
|
||||||
wireUplinks(sw, [primary, alt], 2);
|
|
||||||
} else {
|
|
||||||
wireUplinks(sw, [primary], 2);
|
|
||||||
}
|
|
||||||
registry[sw.id] = sw;
|
|
||||||
switchIds.push(sw.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const networkRackCount = estimateNetworkSlots(computeRackCount, dcTier);
|
|
||||||
return buildDCSummary(switchIds, networkRackCount, registry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function expandDCTopology(
|
export function buildDCNetworkSummary(
|
||||||
existing: DCNetworkSummary,
|
computeRackCount: number,
|
||||||
newRackCount: number,
|
|
||||||
dcTier: DCTier,
|
dcTier: DCTier,
|
||||||
dcId: string,
|
|
||||||
registry: Record<string, NetworkSwitch>,
|
|
||||||
): DCNetworkSummary {
|
): DCNetworkSummary {
|
||||||
if (newRackCount <= 0) return existing;
|
if (computeRackCount <= 0) return emptyDCNetworkSummary();
|
||||||
|
const totalByTier = computeTopologyCounts(computeRackCount, dcTier);
|
||||||
const currentTorCount = existing.totalByTier?.tor ?? 0;
|
const healthyByTier = { ...totalByTier };
|
||||||
const targetTorCount = currentTorCount + newRackCount;
|
|
||||||
|
|
||||||
const t1s = existing.switchIds.map(id => registry[id]).filter((s): s is NetworkSwitch => !!s && s.tier === 't1');
|
|
||||||
const t2s = existing.switchIds.map(id => registry[id]).filter((s): s is NetworkSwitch => !!s && s.tier === 't2');
|
|
||||||
const t3s = existing.switchIds.map(id => registry[id]).filter((s): s is NetworkSwitch => !!s && s.tier === 't3');
|
|
||||||
|
|
||||||
const newIds = [...existing.switchIds];
|
|
||||||
|
|
||||||
const neededT1 = Math.ceil(targetTorCount / SWITCH_TIER_CONFIGS.t1.fanOut);
|
|
||||||
const neededT2 = Math.ceil(neededT1 / SWITCH_TIER_CONFIGS.t2.fanOut);
|
|
||||||
|
|
||||||
while (t2s.length < neededT2) {
|
|
||||||
const sw = createSwitch('t2', dcId, null, null);
|
|
||||||
wireUplinks(sw, t3s, SWITCH_TIER_CONFIGS.t2.uplinkCount);
|
|
||||||
t2s.push(sw);
|
|
||||||
registry[sw.id] = sw;
|
|
||||||
newIds.push(sw.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (t1s.length < neededT1) {
|
|
||||||
const sw = createSwitch('t1', dcId, null, null);
|
|
||||||
wireUplinks(sw, t2s, SWITCH_TIER_CONFIGS.t1.uplinkCount);
|
|
||||||
t1s.push(sw);
|
|
||||||
registry[sw.id] = sw;
|
|
||||||
newIds.push(sw.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < newRackCount; i++) {
|
|
||||||
const torIdx = currentTorCount + i;
|
|
||||||
const sw = createSwitch('tor', dcId, null, null);
|
|
||||||
const primary = t1s[Math.floor(torIdx / SWITCH_TIER_CONFIGS.t1.fanOut)];
|
|
||||||
const altIdx = (Math.floor(torIdx / SWITCH_TIER_CONFIGS.t1.fanOut) + 1) % t1s.length;
|
|
||||||
const alt = t1s[altIdx];
|
|
||||||
if (t1s.length >= 2 && primary !== alt) {
|
|
||||||
wireUplinks(sw, [primary, alt], 2);
|
|
||||||
} else {
|
|
||||||
wireUplinks(sw, [primary], 2);
|
|
||||||
}
|
|
||||||
registry[sw.id] = sw;
|
|
||||||
newIds.push(sw.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const networkRackCount = estimateNetworkSlots(targetTorCount, dcTier);
|
|
||||||
return buildDCSummary(newIds, networkRackCount, registry);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shrinkDCTopology(
|
|
||||||
existing: DCNetworkSummary,
|
|
||||||
removeCount: number,
|
|
||||||
dcTier: DCTier,
|
|
||||||
registry: Record<string, NetworkSwitch>,
|
|
||||||
): DCNetworkSummary {
|
|
||||||
if (removeCount <= 0) return existing;
|
|
||||||
|
|
||||||
const torIds = existing.switchIds.filter(id => registry[id]?.tier === 'tor');
|
|
||||||
const toRemove = new Set(torIds.slice(-removeCount));
|
|
||||||
|
|
||||||
for (const torId of toRemove) {
|
|
||||||
const tor = registry[torId];
|
|
||||||
if (!tor) continue;
|
|
||||||
for (const upId of tor.uplinkIds) {
|
|
||||||
const parent = registry[upId];
|
|
||||||
if (parent) parent.downlinkIds = parent.downlinkIds.filter(id => id !== torId);
|
|
||||||
}
|
|
||||||
delete registry[torId];
|
|
||||||
}
|
|
||||||
|
|
||||||
const remainingIds = existing.switchIds.filter(id => !toRemove.has(id));
|
|
||||||
const remainingTors = remainingIds.filter(id => registry[id]?.tier === 'tor').length;
|
|
||||||
return buildDCSummary(remainingIds, estimateNetworkSlots(remainingTors, dcTier), registry);
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeRackBandwidth(tor: NetworkSwitch, registry: Record<string, NetworkSwitch>): number {
|
|
||||||
if (tor.status !== 'healthy') return 0;
|
|
||||||
|
|
||||||
let minBW = tor.totalUplinks > 0 ? tor.activeUplinks / tor.totalUplinks : 1;
|
|
||||||
if (minBW === 0) return 0;
|
|
||||||
|
|
||||||
const visited = new Set<string>();
|
|
||||||
let current = tor.uplinkIds.filter(id => {
|
|
||||||
const sw = registry[id];
|
|
||||||
return sw && sw.status === 'healthy';
|
|
||||||
});
|
|
||||||
|
|
||||||
while (current.length > 0) {
|
|
||||||
let tierBW = 1;
|
|
||||||
const next: string[] = [];
|
|
||||||
for (const sid of current) {
|
|
||||||
if (visited.has(sid)) continue;
|
|
||||||
visited.add(sid);
|
|
||||||
const sw = registry[sid];
|
|
||||||
if (!sw || sw.status !== 'healthy') continue;
|
|
||||||
const bw = sw.totalUplinks > 0 ? sw.activeUplinks / sw.totalUplinks : 1;
|
|
||||||
tierBW = Math.min(tierBW, bw);
|
|
||||||
for (const upId of sw.uplinkIds) {
|
|
||||||
if (registry[upId]?.status === 'healthy') next.push(upId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
minBW = Math.min(minBW, tierBW);
|
|
||||||
if (minBW === 0) return 0;
|
|
||||||
current = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
return minBW;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildDCSummary(
|
|
||||||
switchIds: string[],
|
|
||||||
networkRackCount: number,
|
|
||||||
registry: Record<string, NetworkSwitch>,
|
|
||||||
): DCNetworkSummary {
|
|
||||||
const totalByTier: Partial<Record<SwitchTier, number>> = {};
|
|
||||||
const healthyByTier: Partial<Record<SwitchTier, number>> = {};
|
|
||||||
let disconnected = 0;
|
|
||||||
let degraded = 0;
|
|
||||||
let bwSum = 0;
|
|
||||||
let torCount = 0;
|
|
||||||
|
|
||||||
for (const sid of switchIds) {
|
|
||||||
const sw = registry[sid];
|
|
||||||
if (!sw) continue;
|
|
||||||
totalByTier[sw.tier] = (totalByTier[sw.tier] ?? 0) + 1;
|
|
||||||
if (sw.status === 'healthy') healthyByTier[sw.tier] = (healthyByTier[sw.tier] ?? 0) + 1;
|
|
||||||
if (sw.tier === 'tor') {
|
|
||||||
torCount++;
|
|
||||||
const bw = computeRackBandwidth(sw, registry);
|
|
||||||
bwSum += bw;
|
|
||||||
if (bw === 0) disconnected++;
|
|
||||||
else if (bw < 1) degraded++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const avgBW = torCount > 0 ? bwSum / torCount : 1;
|
|
||||||
return {
|
return {
|
||||||
switchIds, networkRackCount, totalByTier, healthyByTier,
|
totalByTier, healthyByTier,
|
||||||
racksDisconnected: disconnected, racksDegraded: degraded,
|
repairBatches: [],
|
||||||
averageBandwidth: avgBW, effectiveFlopsFraction: avgBW,
|
networkRackCount: estimateNetworkSlots(computeRackCount, dcTier),
|
||||||
|
racksDisconnected: 0, racksDegraded: 0,
|
||||||
|
averageBandwidth: 1, effectiveFlopsFraction: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Network Tick (failure rolls + repair) ---
|
export function expandDCNetwork(
|
||||||
|
existing: DCNetworkSummary,
|
||||||
|
addedRacks: number,
|
||||||
|
dcTier: DCTier,
|
||||||
|
): DCNetworkSummary {
|
||||||
|
if (addedRacks <= 0) return existing;
|
||||||
|
const oldTor = existing.totalByTier.tor ?? 0;
|
||||||
|
const newTor = oldTor + addedRacks;
|
||||||
|
const newTotal = computeTopologyCounts(newTor, dcTier);
|
||||||
|
const healthyByTier: Partial<Record<SwitchTier, number>> = {};
|
||||||
|
for (const tier of DC_TIERS) {
|
||||||
|
const oldTotal = existing.totalByTier[tier] ?? 0;
|
||||||
|
const oldHealthy = existing.healthyByTier[tier] ?? 0;
|
||||||
|
const added = (newTotal[tier] ?? 0) - oldTotal;
|
||||||
|
healthyByTier[tier] = oldHealthy + Math.max(0, added);
|
||||||
|
}
|
||||||
|
const summary: DCNetworkSummary = {
|
||||||
|
...existing,
|
||||||
|
totalByTier: newTotal,
|
||||||
|
healthyByTier,
|
||||||
|
networkRackCount: estimateNetworkSlots(newTor, dcTier),
|
||||||
|
};
|
||||||
|
return recomputeBandwidth(summary);
|
||||||
|
}
|
||||||
|
|
||||||
function processNetworkTick(
|
export function shrinkDCNetwork(
|
||||||
registry: Record<string, NetworkSwitch>,
|
existing: DCNetworkSummary,
|
||||||
|
removedRacks: number,
|
||||||
|
dcTier: DCTier,
|
||||||
|
): DCNetworkSummary {
|
||||||
|
if (removedRacks <= 0) return existing;
|
||||||
|
const oldTor = existing.totalByTier.tor ?? 0;
|
||||||
|
const newTor = Math.max(0, oldTor - removedRacks);
|
||||||
|
if (newTor === 0) return emptyDCNetworkSummary();
|
||||||
|
const newTotal = computeTopologyCounts(newTor, dcTier);
|
||||||
|
const healthyByTier: Partial<Record<SwitchTier, number>> = {};
|
||||||
|
for (const tier of DC_TIERS) {
|
||||||
|
const nt = newTotal[tier] ?? 0;
|
||||||
|
const oh = existing.healthyByTier[tier] ?? 0;
|
||||||
|
healthyByTier[tier] = Math.min(oh, nt);
|
||||||
|
}
|
||||||
|
const repairBatches = existing.repairBatches.filter(b => {
|
||||||
|
const nt = newTotal[b.tier] ?? 0;
|
||||||
|
const nh = healthyByTier[b.tier] ?? 0;
|
||||||
|
return nh < nt;
|
||||||
|
});
|
||||||
|
const summary: DCNetworkSummary = {
|
||||||
|
...existing,
|
||||||
|
totalByTier: newTotal,
|
||||||
|
healthyByTier,
|
||||||
|
repairBatches,
|
||||||
|
networkRackCount: estimateNetworkSlots(newTor, dcTier),
|
||||||
|
};
|
||||||
|
return recomputeBandwidth(summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeAggregateBandwidth(
|
||||||
|
summary: DCNetworkSummary,
|
||||||
|
redundancyBonus: number,
|
||||||
|
): number {
|
||||||
|
let minBW = 1;
|
||||||
|
for (const tier of DC_TIERS) {
|
||||||
|
const total = summary.totalByTier[tier] ?? 0;
|
||||||
|
if (total === 0) continue;
|
||||||
|
const healthy = summary.healthyByTier[tier] ?? 0;
|
||||||
|
const tierBW = Math.min(1, (healthy + redundancyBonus) / total);
|
||||||
|
if (tierBW < minBW) minBW = tierBW;
|
||||||
|
}
|
||||||
|
return minBW;
|
||||||
|
}
|
||||||
|
|
||||||
|
function recomputeBandwidth(summary: DCNetworkSummary, redundancyBonus = 0): DCNetworkSummary {
|
||||||
|
const avgBW = computeAggregateBandwidth(summary, redundancyBonus);
|
||||||
|
const torTotal = summary.totalByTier.tor ?? 0;
|
||||||
|
const torHealthy = summary.healthyByTier.tor ?? 0;
|
||||||
|
const torFailed = torTotal - torHealthy;
|
||||||
|
const disconnected = avgBW === 0 ? torTotal : torFailed;
|
||||||
|
const degraded = avgBW > 0 && avgBW < 1 ? Math.ceil(torTotal * (1 - avgBW)) - disconnected : 0;
|
||||||
|
return {
|
||||||
|
...summary,
|
||||||
|
averageBandwidth: avgBW,
|
||||||
|
effectiveFlopsFraction: avgBW,
|
||||||
|
racksDisconnected: Math.max(0, disconnected),
|
||||||
|
racksDegraded: Math.max(0, degraded),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function processNetworkForDC(
|
||||||
|
summary: DCNetworkSummary,
|
||||||
networkResearchBonus: number,
|
networkResearchBonus: number,
|
||||||
opsEff: number,
|
opsEff: number,
|
||||||
repairSpeedBonus: number,
|
repairSpeedBonus: number,
|
||||||
hotStandbyTicks: number,
|
hotStandbyTicks: number,
|
||||||
redundancyBonus: number,
|
redundancyBonus: number,
|
||||||
): { switchRepairCosts: number; notifications: TickNotification[]; dirtyDCs: Set<string> } {
|
): { summary: DCNetworkSummary; costs: number; notifications: TickNotification[] } {
|
||||||
|
const torTotal = summary.totalByTier.tor ?? 0;
|
||||||
|
if (torTotal === 0) return { summary, costs: 0, notifications: [] };
|
||||||
|
|
||||||
|
let costs = 0;
|
||||||
const notifications: TickNotification[] = [];
|
const notifications: TickNotification[] = [];
|
||||||
let switchRepairCosts = 0;
|
const healthyByTier = { ...summary.healthyByTier };
|
||||||
const dirtyDCs = new Set<string>();
|
let dirty = false;
|
||||||
|
|
||||||
const healthyByTier: Partial<Record<SwitchTier, NetworkSwitch[]>> = {};
|
for (const tier of DC_TIERS) {
|
||||||
const repairing: NetworkSwitch[] = [];
|
const healthy = healthyByTier[tier] ?? 0;
|
||||||
|
if (healthy <= 0) continue;
|
||||||
for (const sw of Object.values(registry)) {
|
|
||||||
if (sw.status === 'healthy') {
|
|
||||||
(healthyByTier[sw.tier] ??= []).push(sw);
|
|
||||||
} else if (sw.status === 'repairing') {
|
|
||||||
repairing.push(sw);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tiers: SwitchTier[] = ['tor', 't1', 't2', 't3', 't4', 't5'];
|
|
||||||
const newlyFailed: NetworkSwitch[] = [];
|
|
||||||
|
|
||||||
for (const tier of tiers) {
|
|
||||||
const healthy = healthyByTier[tier];
|
|
||||||
if (!healthy || healthy.length === 0) continue;
|
|
||||||
const rate = SWITCH_TIER_CONFIGS[tier].failureRatePerTick * (1 - networkResearchBonus);
|
const rate = SWITCH_TIER_CONFIGS[tier].failureRatePerTick * (1 - networkResearchBonus);
|
||||||
const count = binomialSample(healthy.length, rate);
|
const failed = binomialSample(healthy, rate);
|
||||||
if (count > 0) {
|
if (failed > 0) {
|
||||||
const shuffled = [...healthy].sort(() => Math.random() - 0.5);
|
healthyByTier[tier] = healthy - failed;
|
||||||
for (let i = 0; i < count; i++) {
|
const baseRepair = SWITCH_TIER_CONFIGS[tier].repairBaseTicks;
|
||||||
const sw = shuffled[i];
|
const repairTime = hotStandbyTicks > 0
|
||||||
const baseRepair = SWITCH_TIER_CONFIGS[tier].repairBaseTicks;
|
? hotStandbyTicks
|
||||||
const repairTime = hotStandbyTicks > 0
|
: baseRepair * (1 - repairSpeedBonus);
|
||||||
? hotStandbyTicks
|
summary.repairBatches.push({ tier, count: failed, ticksRemaining: repairTime });
|
||||||
: baseRepair * (1 - repairSpeedBonus);
|
costs += SWITCH_TIER_CONFIGS[tier].baseCost * SWITCH_REPAIR_COST_FRACTION * failed;
|
||||||
sw.status = 'repairing';
|
dirty = true;
|
||||||
sw.repairProgress = 0;
|
|
||||||
sw.repairTotal = repairTime;
|
if (tier === 't3') {
|
||||||
newlyFailed.push(sw);
|
notifications.push({ title: 'Core Network Failure', message: `Tier-3 core switch failed — potential DC disconnect!`, type: 'danger' });
|
||||||
if (sw.dcId) dirtyDCs.add(sw.dcId);
|
} else if (tier === 't2') {
|
||||||
switchRepairCosts += SWITCH_TIER_CONFIGS[tier].baseCost * SWITCH_REPAIR_COST_FRACTION;
|
notifications.push({ title: 'Network Switch Failure', message: `Tier-2 spine switch failed — racks may be degraded.`, type: 'warning' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const sw of repairing) {
|
const remainingBatches: RepairBatch[] = [];
|
||||||
sw.repairProgress += 1 + opsEff * 0.05;
|
for (const batch of summary.repairBatches) {
|
||||||
if (sw.repairProgress >= sw.repairTotal) {
|
const newTicks = batch.ticksRemaining - (1 + opsEff * 0.05);
|
||||||
sw.status = 'healthy';
|
if (newTicks <= 0) {
|
||||||
sw.repairProgress = 0;
|
healthyByTier[batch.tier] = Math.min(
|
||||||
sw.repairTotal = 0;
|
summary.totalByTier[batch.tier] ?? 0,
|
||||||
if (sw.dcId) dirtyDCs.add(sw.dcId);
|
(healthyByTier[batch.tier] ?? 0) + batch.count,
|
||||||
|
);
|
||||||
|
dirty = true;
|
||||||
|
} else {
|
||||||
|
remainingBatches.push({ ...batch, ticksRemaining: newTicks });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dirtyDCs.size > 0) {
|
if (!dirty) return { summary: { ...summary, repairBatches: remainingBatches }, costs, notifications };
|
||||||
for (const sw of Object.values(registry)) {
|
|
||||||
if (sw.uplinkIds.length === 0) continue;
|
|
||||||
if (sw.dcId && !dirtyDCs.has(sw.dcId)) continue;
|
|
||||||
let active = 0;
|
|
||||||
for (const upId of sw.uplinkIds) {
|
|
||||||
if (registry[upId]?.status === 'healthy') active++;
|
|
||||||
}
|
|
||||||
sw.activeUplinks = active;
|
|
||||||
sw.effectiveBandwidth = sw.totalUplinks > 0 ? Math.min(1, (active + redundancyBonus) / sw.totalUplinks) : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const sw of newlyFailed) {
|
const updated: DCNetworkSummary = {
|
||||||
if (sw.tier === 't3') {
|
...summary,
|
||||||
notifications.push({ title: 'Core Network Failure', message: `Tier-3 core switch failed — potential DC disconnect!`, type: 'danger' });
|
healthyByTier,
|
||||||
} else if (sw.tier === 't4') {
|
repairBatches: remainingBatches,
|
||||||
notifications.push({ title: 'Campus Network Failure', message: `Tier-4 campus switch failed — cross-DC degradation!`, type: 'danger' });
|
};
|
||||||
} else if (sw.tier === 't2') {
|
return { summary: recomputeBandwidth(updated, redundancyBonus), costs, notifications };
|
||||||
notifications.push({ title: 'Network Switch Failure', message: `Tier-2 spine switch failed — racks may be degraded.`, type: 'warning' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { switchRepairCosts, notifications, dirtyDCs };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Interconnect Training Multiplier ---
|
// --- Interconnect Training Multiplier ---
|
||||||
@@ -476,14 +321,6 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear
|
|||||||
const hotStandbyTicks = state.research.completedResearch.includes('network-hot-standby') ? 5 : 0;
|
const hotStandbyTicks = state.research.completedResearch.includes('network-hot-standby') ? 5 : 0;
|
||||||
const redundancyBonus = state.research.completedResearch.includes('network-redundancy') ? 1 : 0;
|
const redundancyBonus = state.research.completedResearch.includes('network-redundancy') ? 1 : 0;
|
||||||
|
|
||||||
// Mutate registry in-place — infrastructure returns a new state anyway
|
|
||||||
const registry = state.infrastructure.switchRegistry;
|
|
||||||
|
|
||||||
// Process network failures/repairs globally
|
|
||||||
const netResult = processNetworkTick(registry, networkResearchBonus, opsEff, repairSpeedBonus, hotStandbyTicks, redundancyBonus);
|
|
||||||
repairCosts += netResult.switchRepairCosts;
|
|
||||||
if (netResult.notifications.length > 0) notifications.push(...netResult.notifications);
|
|
||||||
|
|
||||||
let totalFlops = 0;
|
let totalFlops = 0;
|
||||||
let totalTrainingFlops = 0;
|
let totalTrainingFlops = 0;
|
||||||
let totalInferenceFlops = 0;
|
let totalInferenceFlops = 0;
|
||||||
@@ -541,8 +378,6 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear
|
|||||||
if (rs.progress >= rs.total) {
|
if (rs.progress >= rs.total) {
|
||||||
if (rs.phase === 'decommissioning') {
|
if (rs.phase === 'decommissioning') {
|
||||||
const installTotal = cohortStageTotal('installation', rs.toSkuId, rs.racksRemaining);
|
const installTotal = cohortStageTotal('installation', rs.toSkuId, rs.racksRemaining);
|
||||||
// Clear DC topology on retrofit
|
|
||||||
for (const sid of dc.networkSummary.switchIds) delete registry[sid];
|
|
||||||
return {
|
return {
|
||||||
...dc,
|
...dc,
|
||||||
computeRacksOnline: 0,
|
computeRacksOnline: 0,
|
||||||
@@ -636,10 +471,11 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear
|
|||||||
// Expand topology for newly onlined racks
|
// Expand topology for newly onlined racks
|
||||||
let networkSummary = dc.networkSummary;
|
let networkSummary = dc.networkSummary;
|
||||||
if (racksJustOnlined > 0) {
|
if (racksJustOnlined > 0) {
|
||||||
if (networkSummary.switchIds.length === 0) {
|
const torTotal = networkSummary.totalByTier.tor ?? 0;
|
||||||
networkSummary = buildDCTopology(computeRacksOnline, dc.tier, dc.id, registry);
|
if (torTotal === 0) {
|
||||||
|
networkSummary = buildDCNetworkSummary(computeRacksOnline, dc.tier);
|
||||||
} else {
|
} else {
|
||||||
networkSummary = expandDCTopology(networkSummary, racksJustOnlined, dc.tier, dc.id, registry);
|
networkSummary = expandDCNetwork(networkSummary, racksJustOnlined, dc.tier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -660,18 +496,20 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear
|
|||||||
stageTotal: cohortStageTotal('repair', dc.rackSkuId, prodFailures),
|
stageTotal: cohortStageTotal('repair', dc.rackSkuId, prodFailures),
|
||||||
repairCount: 0,
|
repairCount: 0,
|
||||||
});
|
});
|
||||||
networkSummary = shrinkDCTopology(networkSummary, prodFailures, dc.tier, registry);
|
networkSummary = shrinkDCNetwork(networkSummary, prodFailures, dc.tier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repairCosts += dcRepairCosts;
|
repairCosts += dcRepairCosts;
|
||||||
|
|
||||||
// Recompute DC network summary after failures/repairs (only if this DC's switches changed)
|
// Process per-DC network failures and repairs (aggregate model)
|
||||||
if (netResult.dirtyDCs.has(dc.id) && networkSummary.switchIds.length > 0) {
|
const netResult = processNetworkForDC(
|
||||||
networkSummary = buildDCSummary(
|
networkSummary, networkResearchBonus, opsEff,
|
||||||
networkSummary.switchIds, networkSummary.networkRackCount, registry,
|
repairSpeedBonus, hotStandbyTicks, redundancyBonus,
|
||||||
);
|
);
|
||||||
}
|
networkSummary = netResult.summary;
|
||||||
|
repairCosts += netResult.costs;
|
||||||
|
if (netResult.notifications.length > 0) notifications.push(...netResult.notifications);
|
||||||
|
|
||||||
// Rackdown: detect recovery (previously disconnected racks now have connectivity)
|
// Rackdown: detect recovery (previously disconnected racks now have connectivity)
|
||||||
const prevDisconnected = dc.networkSummary.racksDisconnected;
|
const prevDisconnected = dc.networkSummary.racksDisconnected;
|
||||||
@@ -680,7 +518,7 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear
|
|||||||
if (currDisconnected < prevDisconnected && dc.rackSkuId) {
|
if (currDisconnected < prevDisconnected && dc.rackSkuId) {
|
||||||
const recovered = prevDisconnected - currDisconnected;
|
const recovered = prevDisconnected - currDisconnected;
|
||||||
computeRacksOnline -= recovered;
|
computeRacksOnline -= recovered;
|
||||||
networkSummary = shrinkDCTopology(networkSummary, recovered, dc.tier, registry);
|
networkSummary = shrinkDCNetwork(networkSummary, recovered, dc.tier);
|
||||||
updatedCohorts.push({
|
updatedCohorts.push({
|
||||||
id: `netrecovery-${dc.id}-${Date.now()}`,
|
id: `netrecovery-${dc.id}-${Date.now()}`,
|
||||||
count: recovered, skuId: dc.rackSkuId,
|
count: recovered, skuId: dc.rackSkuId,
|
||||||
@@ -688,10 +526,6 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear
|
|||||||
stageTotal: cohortStageTotal('testing', dc.rackSkuId, recovered),
|
stageTotal: cohortStageTotal('testing', dc.rackSkuId, recovered),
|
||||||
repairCount: 0,
|
repairCount: 0,
|
||||||
});
|
});
|
||||||
// Recompute summary after shrink
|
|
||||||
networkSummary = buildDCSummary(
|
|
||||||
networkSummary.switchIds, networkSummary.networkRackCount, registry,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute DC aggregates
|
// Compute DC aggregates
|
||||||
@@ -789,8 +623,6 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear
|
|||||||
if (totalRacks <= 0) return dc;
|
if (totalRacks <= 0) return dc;
|
||||||
const oldSku = RACK_SKU_CONFIGS[dc.rackSkuId as RackSkuId];
|
const oldSku = RACK_SKU_CONFIGS[dc.rackSkuId as RackSkuId];
|
||||||
const decommTicks = Math.ceil(oldSku.pipelineTimeTicks.installation * (1 + COHORT_SCALE_FACTOR * totalRacks));
|
const decommTicks = Math.ceil(oldSku.pipelineTimeTicks.installation * (1 + COHORT_SCALE_FACTOR * totalRacks));
|
||||||
// Clear topology on retrofit start
|
|
||||||
for (const sid of dc.networkSummary.switchIds) delete registry[sid];
|
|
||||||
return {
|
return {
|
||||||
...dc,
|
...dc,
|
||||||
status: 'retrofitting' as const,
|
status: 'retrofitting' as const,
|
||||||
@@ -827,7 +659,6 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear
|
|||||||
return {
|
return {
|
||||||
infrastructure: {
|
infrastructure: {
|
||||||
clusters,
|
clusters,
|
||||||
switchRegistry: registry,
|
|
||||||
totalFlops,
|
totalFlops,
|
||||||
totalTrainingFlops,
|
totalTrainingFlops,
|
||||||
totalInferenceFlops,
|
totalInferenceFlops,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { GameState, MarketState, BenchmarkResult } from '@ai-tycoon/shared';
|
import type { GameState, MarketState, ModelCapabilities } from '@ai-tycoon/shared';
|
||||||
import { CONSUMER_TOKENS_PER_SUBSCRIBER, API_TOKENS_PER_DEVELOPER_PER_TICK, BATCH_API_DEMAND_PER_DEV, makeInitialServingMetrics } from '@ai-tycoon/shared';
|
import { CONSUMER_TOKENS_PER_SUBSCRIBER, API_TOKENS_PER_DEVELOPER_PER_TICK, BATCH_API_DEMAND_PER_DEV, makeInitialServingMetrics } from '@ai-tycoon/shared';
|
||||||
import type { TrafficPriority, TierServingMetrics } from '@ai-tycoon/shared';
|
import type { TrafficPriority, TierServingMetrics } from '@ai-tycoon/shared';
|
||||||
import { BENCHMARKS } from '../../data/benchmarks';
|
|
||||||
import { computeSeasonal } from './seasonalSystem';
|
import { computeSeasonal } from './seasonalSystem';
|
||||||
import { updateObsolescence } from './obsolescenceSystem';
|
import { updateObsolescence } from './obsolescenceSystem';
|
||||||
import { buildPlayerProfile, buildCompetitorProfile, computeMarketShares, updateTAMGrowth } from './tamSystem';
|
import { buildPlayerProfile, buildCompetitorProfile, computeMarketShares, updateTAMGrowth } from './tamSystem';
|
||||||
@@ -21,31 +20,30 @@ export interface MarketTickResult {
|
|||||||
totalTokenDemand: number;
|
totalTokenDemand: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SEGMENT_CAPABILITY_WEIGHTS: Record<string, Partial<Record<keyof ModelCapabilities, number>>> = {
|
||||||
|
consumer: { creative: 0.35, knowledge: 0.25, reasoning: 0.15, multimodal: 0.15, coding: 0.05, agents: 0.05 },
|
||||||
|
enterprise: { reasoning: 0.25, coding: 0.20, agents: 0.20, knowledge: 0.15, math: 0.10, multimodal: 0.10 },
|
||||||
|
developer: { coding: 0.35, reasoning: 0.20, agents: 0.20, math: 0.15, knowledge: 0.10 },
|
||||||
|
research: { reasoning: 0.30, math: 0.30, knowledge: 0.20, coding: 0.10, agents: 0.10 },
|
||||||
|
};
|
||||||
|
|
||||||
function getSegmentQuality(
|
function getSegmentQuality(
|
||||||
segment: 'consumer' | 'enterprise' | 'developer' | 'research',
|
segment: 'consumer' | 'enterprise' | 'developer' | 'research',
|
||||||
benchmarkResults: BenchmarkResult[],
|
capabilities: ModelCapabilities,
|
||||||
fallbackScore: number,
|
fallbackScore: number,
|
||||||
): number {
|
): number {
|
||||||
if (benchmarkResults.length === 0) return fallbackScore / 100;
|
const weights = SEGMENT_CAPABILITY_WEIGHTS[segment];
|
||||||
|
if (!weights) return fallbackScore / 100;
|
||||||
const bestByBenchmark = new Map<string, number>();
|
|
||||||
for (const r of benchmarkResults) {
|
|
||||||
const prev = bestByBenchmark.get(r.benchmarkId) ?? 0;
|
|
||||||
if (r.score > prev) bestByBenchmark.set(r.benchmarkId, r.score);
|
|
||||||
}
|
|
||||||
|
|
||||||
let weightedSum = 0;
|
let weightedSum = 0;
|
||||||
let totalWeight = 0;
|
let totalWeight = 0;
|
||||||
for (const bench of BENCHMARKS) {
|
for (const [cap, weight] of Object.entries(weights)) {
|
||||||
const score = bestByBenchmark.get(bench.id);
|
const score = capabilities[cap as keyof ModelCapabilities] ?? 0;
|
||||||
if (score == null) continue;
|
if (score > 0) {
|
||||||
const weight = bench.marketRelevance[segment];
|
weightedSum += (score / 100) * weight;
|
||||||
weightedSum += (score / 100) * weight;
|
totalWeight += weight;
|
||||||
totalWeight += weight;
|
}
|
||||||
}
|
}
|
||||||
|
return totalWeight > 0 ? weightedSum / totalWeight : fallbackScore / 100;
|
||||||
if (totalWeight === 0) return fallbackScore / 100;
|
|
||||||
return weightedSum / totalWeight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processMarketV2(
|
export function processMarketV2(
|
||||||
@@ -54,9 +52,11 @@ export function processMarketV2(
|
|||||||
effectiveInferenceFlops?: number,
|
effectiveInferenceFlops?: number,
|
||||||
researchBonuses?: ResearchBonuses,
|
researchBonuses?: ResearchBonuses,
|
||||||
): MarketTickResult {
|
): MarketTickResult {
|
||||||
const consumerQuality = getSegmentQuality('consumer', state.models.benchmarkResults, state.models.bestDeployedModelScore);
|
const caps = state.models.bestDeployedCapabilities;
|
||||||
const enterpriseQuality = getSegmentQuality('enterprise', state.models.benchmarkResults, state.models.bestDeployedModelScore);
|
const hasDeployed = state.models.bestDeployedModelScore > 0;
|
||||||
const modelQuality = state.models.benchmarkResults.length > 0
|
const consumerQuality = getSegmentQuality('consumer', caps, state.models.bestDeployedModelScore);
|
||||||
|
const enterpriseQuality = getSegmentQuality('enterprise', caps, state.models.bestDeployedModelScore);
|
||||||
|
const modelQuality = hasDeployed
|
||||||
? (consumerQuality + enterpriseQuality) / 2
|
? (consumerQuality + enterpriseQuality) / 2
|
||||||
: state.models.bestDeployedModelScore / 100;
|
: state.models.bestDeployedModelScore / 100;
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ export function processMarketV2(
|
|||||||
const productResult = processProductLines(
|
const productResult = processProductLines(
|
||||||
state.market.codeAssistant,
|
state.market.codeAssistant,
|
||||||
state.market.agentsPlatform,
|
state.market.agentsPlatform,
|
||||||
state.models.benchmarkResults,
|
caps,
|
||||||
playerDevCustomers,
|
playerDevCustomers,
|
||||||
playerEntCustomers,
|
playerEntCustomers,
|
||||||
seasonal.multipliers.consumer,
|
seasonal.multipliers.consumer,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CodeAssistantState, AgentsPlatformState, BenchmarkResult } from '@ai-tycoon/shared';
|
import type { CodeAssistantState, AgentsPlatformState, ModelCapabilities } from '@ai-tycoon/shared';
|
||||||
import {
|
import {
|
||||||
CODE_ASSISTANT_MIN_CODING_SCORE,
|
CODE_ASSISTANT_MIN_CODING_SCORE,
|
||||||
CODE_ASSISTANT_BASE_ADOPTION_RATE,
|
CODE_ASSISTANT_BASE_ADOPTION_RATE,
|
||||||
@@ -7,27 +7,6 @@ import {
|
|||||||
AGENTS_PLATFORM_BASE_ADOPTION_RATE,
|
AGENTS_PLATFORM_BASE_ADOPTION_RATE,
|
||||||
AGENTS_PLATFORM_CHURN_RATE,
|
AGENTS_PLATFORM_CHURN_RATE,
|
||||||
} from '@ai-tycoon/shared';
|
} from '@ai-tycoon/shared';
|
||||||
import { BENCHMARKS } from '../../data/benchmarks';
|
|
||||||
|
|
||||||
function getBenchmarkScore(benchmarkId: string, results: BenchmarkResult[]): number {
|
|
||||||
let best = 0;
|
|
||||||
for (const r of results) {
|
|
||||||
if (r.benchmarkId === benchmarkId && r.score > best) best = r.score;
|
|
||||||
}
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCodingScore(results: BenchmarkResult[]): number {
|
|
||||||
const codeBench = BENCHMARKS.find(b => b.id === 'codeforce');
|
|
||||||
if (!codeBench) return 0;
|
|
||||||
return getBenchmarkScore(codeBench.id, results);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAgentsScore(results: BenchmarkResult[]): number {
|
|
||||||
const agentBench = BENCHMARKS.find(b => b.id === 'agentarena');
|
|
||||||
if (!agentBench) return 0;
|
|
||||||
return getBenchmarkScore(agentBench.id, results);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProductLineResult {
|
export interface ProductLineResult {
|
||||||
codeAssistant: CodeAssistantState;
|
codeAssistant: CodeAssistantState;
|
||||||
@@ -41,7 +20,7 @@ export interface ProductLineResult {
|
|||||||
export function processProductLines(
|
export function processProductLines(
|
||||||
ca: CodeAssistantState,
|
ca: CodeAssistantState,
|
||||||
ap: AgentsPlatformState,
|
ap: AgentsPlatformState,
|
||||||
benchmarkResults: BenchmarkResult[],
|
capabilities: ModelCapabilities,
|
||||||
playerDevCustomers: number,
|
playerDevCustomers: number,
|
||||||
playerEntCustomers: number,
|
playerEntCustomers: number,
|
||||||
seasonalConsumerMult: number,
|
seasonalConsumerMult: number,
|
||||||
@@ -53,7 +32,7 @@ export function processProductLines(
|
|||||||
let apRevenue = 0;
|
let apRevenue = 0;
|
||||||
|
|
||||||
// --- Code Assistant ---
|
// --- Code Assistant ---
|
||||||
updatedCA.qualityScore = getCodingScore(benchmarkResults);
|
updatedCA.qualityScore = capabilities.coding;
|
||||||
if (updatedCA.isUnlocked && updatedCA.isActive && updatedCA.qualityScore >= CODE_ASSISTANT_MIN_CODING_SCORE) {
|
if (updatedCA.isUnlocked && updatedCA.isActive && updatedCA.qualityScore >= CODE_ASSISTANT_MIN_CODING_SCORE) {
|
||||||
const qualityFactor = updatedCA.qualityScore / 100;
|
const qualityFactor = updatedCA.qualityScore / 100;
|
||||||
const priceAttr = Math.max(0.1, 1 - updatedCA.pricePerSeat / 50);
|
const priceAttr = Math.max(0.1, 1 - updatedCA.pricePerSeat / 50);
|
||||||
@@ -70,7 +49,7 @@ export function processProductLines(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Agents Platform ---
|
// --- Agents Platform ---
|
||||||
updatedAP.qualityScore = getAgentsScore(benchmarkResults);
|
updatedAP.qualityScore = capabilities.agents;
|
||||||
if (updatedAP.isUnlocked && updatedAP.isActive && updatedAP.qualityScore >= AGENTS_PLATFORM_MIN_AGENTS_SCORE) {
|
if (updatedAP.isUnlocked && updatedAP.isActive && updatedAP.qualityScore >= AGENTS_PLATFORM_MIN_AGENTS_SCORE) {
|
||||||
const qualityFactor = updatedAP.qualityScore / 100;
|
const qualityFactor = updatedAP.qualityScore / 100;
|
||||||
const priceAttr = Math.max(0.1, 1 - updatedAP.pricePerSeat / 250);
|
const priceAttr = Math.max(0.1, 1 - updatedAP.pricePerSeat / 250);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
ModelUtilizationEntry,
|
ModelUtilizationEntry,
|
||||||
BatchApiState,
|
BatchApiState,
|
||||||
} from '@ai-tycoon/shared';
|
} from '@ai-tycoon/shared';
|
||||||
import type { BaseModel, ModelVariant, ModelFamily, ModelsState, SizeTier } from '@ai-tycoon/shared';
|
import type { BaseModel, ModelsState, SizeTier } from '@ai-tycoon/shared';
|
||||||
import {
|
import {
|
||||||
MODEL_SIZE_THROUGHPUT_SCALER,
|
MODEL_SIZE_THROUGHPUT_SCALER,
|
||||||
MOE_SPEED_MULTIPLIER,
|
MOE_SPEED_MULTIPLIER,
|
||||||
@@ -62,73 +62,133 @@ export interface ServingPipelineResult {
|
|||||||
batchRevenue: number;
|
batchRevenue: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CachedSlot {
|
||||||
|
modelId: string;
|
||||||
|
modelName: string;
|
||||||
|
sizeTier: SizeTier;
|
||||||
|
isVariant: boolean;
|
||||||
|
quantization: string | null;
|
||||||
|
qualityScore: number;
|
||||||
|
speedMultiplier: number;
|
||||||
|
throughputMultiplier: number;
|
||||||
|
isMoE: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedDeploymentVersion = -1;
|
||||||
|
let cachedSlots: CachedSlot[] = [];
|
||||||
|
const fleetOutput: ModelServingSlot[] = [];
|
||||||
|
|
||||||
|
const mainRemaining = new Map<string, number>();
|
||||||
|
const mainUsed = new Map<string, number>();
|
||||||
|
const entRemaining = new Map<string, number>();
|
||||||
|
const entUsed = new Map<string, number>();
|
||||||
|
|
||||||
|
let cachedUtilization: ModelUtilizationEntry[] = [];
|
||||||
|
|
||||||
|
export function resetFleetCache(): void {
|
||||||
|
cachedDeploymentVersion = -1;
|
||||||
|
cachedSlots.length = 0;
|
||||||
|
fleetOutput.length = 0;
|
||||||
|
mainRemaining.clear();
|
||||||
|
mainUsed.clear();
|
||||||
|
entRemaining.clear();
|
||||||
|
entUsed.clear();
|
||||||
|
cachedUtilization.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
function buildModelFleet(
|
function buildModelFleet(
|
||||||
modelsState: ModelsState,
|
modelsState: ModelsState,
|
||||||
effectiveInferenceFlops: number,
|
effectiveInferenceFlops: number,
|
||||||
): ModelServingSlot[] {
|
): ModelServingSlot[] {
|
||||||
const slots: ModelServingSlot[] = [];
|
const version = modelsState.deploymentVersion;
|
||||||
|
|
||||||
const deployedBases: BaseModel[] = [];
|
if (version !== cachedDeploymentVersion) {
|
||||||
const baseModelById = new Map<string, BaseModel>();
|
cachedSlots.length = 0;
|
||||||
for (const m of modelsState.baseModels) {
|
|
||||||
if (m.isDeployed) deployedBases.push(m);
|
|
||||||
baseModelById.set(m.id, m);
|
|
||||||
}
|
|
||||||
|
|
||||||
const deployedVariants: { variant: ModelVariant; baseModel: BaseModel }[] = [];
|
const baseModelById = new Map<string, BaseModel>();
|
||||||
for (const family of modelsState.families) {
|
for (const m of modelsState.baseModels) {
|
||||||
for (const variant of family.variants) {
|
baseModelById.set(m.id, m);
|
||||||
if (!variant.isDeployed) continue;
|
if (!m.isDeployed) continue;
|
||||||
const base = baseModelById.get(variant.baseModelId);
|
const sizeFactor = MODEL_SIZE_THROUGHPUT_SCALER[m.sizeTier] ?? 1.0;
|
||||||
if (base) deployedVariants.push({ variant, baseModel: base });
|
const moeFactor = m.architecture.type === 'moe' ? MOE_SPEED_MULTIPLIER : 1.0;
|
||||||
|
cachedSlots.push({
|
||||||
|
modelId: m.id,
|
||||||
|
modelName: m.name,
|
||||||
|
sizeTier: m.sizeTier,
|
||||||
|
isVariant: false,
|
||||||
|
quantization: null,
|
||||||
|
qualityScore: m.rawCapability / 100,
|
||||||
|
speedMultiplier: moeFactor,
|
||||||
|
throughputMultiplier: FLOPS_TO_TOKENS_MULTIPLIER * sizeFactor * moeFactor,
|
||||||
|
isMoE: m.architecture.type === 'moe',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const family of modelsState.families) {
|
||||||
|
for (const variant of family.variants) {
|
||||||
|
if (!variant.isDeployed) continue;
|
||||||
|
const base = baseModelById.get(variant.baseModelId);
|
||||||
|
if (!base) continue;
|
||||||
|
const sizeFactor = MODEL_SIZE_THROUGHPUT_SCALER[base.sizeTier] ?? 1.0;
|
||||||
|
const moeFactor = variant.architecture.type === 'moe' ? MOE_SPEED_MULTIPLIER : 1.0;
|
||||||
|
const quantConfig = variant.quantization ? QUANTIZATION_CONFIGS[variant.quantization] : null;
|
||||||
|
const quantSpeedFactor = quantConfig?.speedMultiplier ?? 1.0;
|
||||||
|
const qualityRetention = quantConfig?.qualityRetention ?? 1.0;
|
||||||
|
cachedSlots.push({
|
||||||
|
modelId: variant.id,
|
||||||
|
modelName: variant.name,
|
||||||
|
sizeTier: base.sizeTier,
|
||||||
|
isVariant: true,
|
||||||
|
quantization: variant.quantization ?? null,
|
||||||
|
qualityScore: (base.rawCapability / 100) * qualityRetention,
|
||||||
|
speedMultiplier: moeFactor * quantSpeedFactor,
|
||||||
|
throughputMultiplier: FLOPS_TO_TOKENS_MULTIPLIER * sizeFactor * moeFactor * quantSpeedFactor,
|
||||||
|
isMoE: variant.architecture.type === 'moe',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedDeploymentVersion = version;
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalDeployed = deployedBases.length + deployedVariants.length;
|
const totalDeployed = cachedSlots.length;
|
||||||
if (totalDeployed === 0 || effectiveInferenceFlops <= 0) return slots;
|
if (totalDeployed === 0 || effectiveInferenceFlops <= 0) {
|
||||||
|
fleetOutput.length = 0;
|
||||||
|
return fleetOutput;
|
||||||
|
}
|
||||||
|
|
||||||
const flopsPerModel = effectiveInferenceFlops / totalDeployed;
|
const flopsPerModel = effectiveInferenceFlops / totalDeployed;
|
||||||
|
|
||||||
for (const model of deployedBases) {
|
fleetOutput.length = totalDeployed;
|
||||||
const sizeFactor = MODEL_SIZE_THROUGHPUT_SCALER[model.sizeTier] ?? 1.0;
|
for (let i = 0; i < totalDeployed; i++) {
|
||||||
const moeFactor = model.architecture.type === 'moe' ? MOE_SPEED_MULTIPLIER : 1.0;
|
const cs = cachedSlots[i];
|
||||||
const throughput = flopsPerModel * FLOPS_TO_TOKENS_MULTIPLIER * sizeFactor * moeFactor;
|
const existing = fleetOutput[i];
|
||||||
|
if (existing) {
|
||||||
slots.push({
|
existing.modelId = cs.modelId;
|
||||||
modelId: model.id,
|
existing.modelName = cs.modelName;
|
||||||
modelName: model.name,
|
existing.sizeTier = cs.sizeTier;
|
||||||
sizeTier: model.sizeTier,
|
existing.isVariant = cs.isVariant;
|
||||||
isVariant: false,
|
existing.quantization = cs.quantization;
|
||||||
quantization: null,
|
existing.qualityScore = cs.qualityScore;
|
||||||
qualityScore: model.rawCapability / 100,
|
existing.speedMultiplier = cs.speedMultiplier;
|
||||||
speedMultiplier: moeFactor,
|
existing.throughputCapacity = flopsPerModel * cs.throughputMultiplier;
|
||||||
throughputCapacity: throughput,
|
existing.isMoE = cs.isMoE;
|
||||||
isMoE: model.architecture.type === 'moe',
|
} else {
|
||||||
});
|
fleetOutput[i] = {
|
||||||
|
modelId: cs.modelId,
|
||||||
|
modelName: cs.modelName,
|
||||||
|
sizeTier: cs.sizeTier,
|
||||||
|
isVariant: cs.isVariant,
|
||||||
|
quantization: cs.quantization,
|
||||||
|
qualityScore: cs.qualityScore,
|
||||||
|
speedMultiplier: cs.speedMultiplier,
|
||||||
|
throughputCapacity: flopsPerModel * cs.throughputMultiplier,
|
||||||
|
isMoE: cs.isMoE,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const { variant, baseModel } of deployedVariants) {
|
return fleetOutput;
|
||||||
const sizeFactor = MODEL_SIZE_THROUGHPUT_SCALER[baseModel.sizeTier] ?? 1.0;
|
|
||||||
const moeFactor = variant.architecture.type === 'moe' ? MOE_SPEED_MULTIPLIER : 1.0;
|
|
||||||
const quantConfig = variant.quantization ? QUANTIZATION_CONFIGS[variant.quantization] : null;
|
|
||||||
const quantSpeedFactor = quantConfig?.speedMultiplier ?? 1.0;
|
|
||||||
const qualityRetention = quantConfig?.qualityRetention ?? 1.0;
|
|
||||||
const throughput = flopsPerModel * FLOPS_TO_TOKENS_MULTIPLIER * sizeFactor * moeFactor * quantSpeedFactor;
|
|
||||||
|
|
||||||
slots.push({
|
|
||||||
modelId: variant.id,
|
|
||||||
modelName: variant.name,
|
|
||||||
sizeTier: baseModel.sizeTier,
|
|
||||||
isVariant: true,
|
|
||||||
quantization: variant.quantization ?? null,
|
|
||||||
qualityScore: (baseModel.rawCapability / 100) * qualityRetention,
|
|
||||||
speedMultiplier: moeFactor * quantSpeedFactor,
|
|
||||||
throughputCapacity: throughput,
|
|
||||||
isMoE: variant.architecture.type === 'moe',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return slots;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortFleetByStrategy(
|
function sortFleetByStrategy(
|
||||||
@@ -136,24 +196,23 @@ function sortFleetByStrategy(
|
|||||||
strategy: string,
|
strategy: string,
|
||||||
overallUtilization: number,
|
overallUtilization: number,
|
||||||
): ModelServingSlot[] {
|
): ModelServingSlot[] {
|
||||||
const sorted = [...fleet];
|
|
||||||
switch (strategy) {
|
switch (strategy) {
|
||||||
case 'quality-first':
|
case 'quality-first':
|
||||||
sorted.sort((a, b) => b.qualityScore - a.qualityScore);
|
fleet.sort((a, b) => b.qualityScore - a.qualityScore);
|
||||||
break;
|
break;
|
||||||
case 'speed-first':
|
case 'speed-first':
|
||||||
sorted.sort((a, b) => b.throughputCapacity - a.throughputCapacity);
|
fleet.sort((a, b) => b.throughputCapacity - a.throughputCapacity);
|
||||||
break;
|
break;
|
||||||
case 'balanced':
|
case 'balanced':
|
||||||
default:
|
default:
|
||||||
if (overallUtilization > 0.8) {
|
if (overallUtilization > 0.8) {
|
||||||
sorted.sort((a, b) => b.throughputCapacity - a.throughputCapacity);
|
fleet.sort((a, b) => b.throughputCapacity - a.throughputCapacity);
|
||||||
} else {
|
} else {
|
||||||
sorted.sort((a, b) => b.qualityScore - a.qualityScore);
|
fleet.sort((a, b) => b.qualityScore - a.qualityScore);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return sorted;
|
return fleet;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FleetState {
|
interface FleetState {
|
||||||
@@ -250,7 +309,8 @@ export function processServingPipeline(input: ServingPipelineInput): ServingPipe
|
|||||||
const { modelsState, effectiveInferenceFlops, overloadPolicy, demandByTier, batchApi, modelQuality, researchUnlocks } = input;
|
const { modelsState, effectiveInferenceFlops, overloadPolicy, demandByTier, batchApi, modelQuality, researchUnlocks } = input;
|
||||||
|
|
||||||
const fleet = buildModelFleet(modelsState, effectiveInferenceFlops);
|
const fleet = buildModelFleet(modelsState, effectiveInferenceFlops);
|
||||||
const totalFleetCapacity = fleet.reduce((sum, s) => sum + s.throughputCapacity, 0);
|
let totalFleetCapacity = 0;
|
||||||
|
for (const s of fleet) totalFleetCapacity += s.throughputCapacity;
|
||||||
|
|
||||||
if (fleet.length === 0 || totalFleetCapacity <= 0) {
|
if (fleet.length === 0 || totalFleetCapacity <= 0) {
|
||||||
const metrics = makeInitialServingMetrics();
|
const metrics = makeInitialServingMetrics();
|
||||||
@@ -275,7 +335,7 @@ export function processServingPipeline(input: ServingPipelineInput): ServingPipe
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalDemand = Object.values(demandByTier).reduce((s, v) => s + v, 0);
|
const totalDemand = demandByTier.enterprise + demandByTier['api-paid'] + demandByTier['consumer-paid'] + demandByTier['api-free'] + demandByTier['consumer-free'];
|
||||||
const overallUtilization = totalFleetCapacity > 0 ? totalDemand / totalFleetCapacity : 0;
|
const overallUtilization = totalFleetCapacity > 0 ? totalDemand / totalFleetCapacity : 0;
|
||||||
|
|
||||||
const effectiveStrategy = researchUnlocks.servingRoutingUnlocked
|
const effectiveStrategy = researchUnlocks.servingRoutingUnlocked
|
||||||
@@ -284,10 +344,13 @@ export function processServingPipeline(input: ServingPipelineInput): ServingPipe
|
|||||||
|
|
||||||
const sortedFleet = sortFleetByStrategy(fleet, effectiveStrategy, overallUtilization);
|
const sortedFleet = sortFleetByStrategy(fleet, effectiveStrategy, overallUtilization);
|
||||||
|
|
||||||
const fleetState: FleetState = {
|
mainRemaining.clear();
|
||||||
remaining: new Map(fleet.map(s => [s.modelId, s.throughputCapacity])),
|
mainUsed.clear();
|
||||||
used: new Map(fleet.map(s => [s.modelId, 0])),
|
for (const s of fleet) {
|
||||||
};
|
mainRemaining.set(s.modelId, s.throughputCapacity);
|
||||||
|
mainUsed.set(s.modelId, 0);
|
||||||
|
}
|
||||||
|
const fleetState: FleetState = { remaining: mainRemaining, used: mainUsed };
|
||||||
|
|
||||||
const reservedCapacity = totalFleetCapacity * overloadPolicy.enterpriseReservation;
|
const reservedCapacity = totalFleetCapacity * overloadPolicy.enterpriseReservation;
|
||||||
const enterpriseDemand = demandByTier['enterprise'] ?? 0;
|
const enterpriseDemand = demandByTier['enterprise'] ?? 0;
|
||||||
@@ -310,10 +373,13 @@ export function processServingPipeline(input: ServingPipelineInput): ServingPipe
|
|||||||
const nonEnterpriseTiers = effectivePriorityOrder.filter(t => t !== 'enterprise');
|
const nonEnterpriseTiers = effectivePriorityOrder.filter(t => t !== 'enterprise');
|
||||||
|
|
||||||
if (enterpriseDemand > 0) {
|
if (enterpriseDemand > 0) {
|
||||||
const enterpriseFleetState: FleetState = {
|
entRemaining.clear();
|
||||||
remaining: new Map(fleet.map(s => [s.modelId, s.throughputCapacity])),
|
entUsed.clear();
|
||||||
used: new Map(fleet.map(s => [s.modelId, 0])),
|
for (const s of fleet) {
|
||||||
};
|
entRemaining.set(s.modelId, s.throughputCapacity);
|
||||||
|
entUsed.set(s.modelId, 0);
|
||||||
|
}
|
||||||
|
const enterpriseFleetState: FleetState = { remaining: entRemaining, used: entUsed };
|
||||||
|
|
||||||
const reserveLimit = reservedCapacity > 0 ? reservedCapacity : totalFleetCapacity;
|
const reserveLimit = reservedCapacity > 0 ? reservedCapacity : totalFleetCapacity;
|
||||||
let budgetLeft = reserveLimit;
|
let budgetLeft = reserveLimit;
|
||||||
@@ -334,10 +400,10 @@ export function processServingPipeline(input: ServingPipelineInput): ServingPipe
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const slot of fleet) {
|
for (const slot of fleet) {
|
||||||
const entUsed = enterpriseFleetState.used.get(slot.modelId) ?? 0;
|
const entUsedForModel = enterpriseFleetState.used.get(slot.modelId) ?? 0;
|
||||||
const mainRemaining = fleetState.remaining.get(slot.modelId) ?? 0;
|
const mainRemainingForModel = fleetState.remaining.get(slot.modelId) ?? 0;
|
||||||
fleetState.remaining.set(slot.modelId, Math.max(0, mainRemaining - entUsed + (reservedCapacity > 0 ? reservedCapacity / fleet.length : 0)));
|
fleetState.remaining.set(slot.modelId, Math.max(0, mainRemainingForModel - entUsedForModel + (reservedCapacity > 0 ? reservedCapacity / fleet.length : 0)));
|
||||||
fleetState.used.set(slot.modelId, entUsed);
|
fleetState.used.set(slot.modelId, entUsedForModel);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tierResults['enterprise'] = { demandTokens: 0, servedTokens: 0, queuedTokens: 0, rejectedTokens: 0, degradedTokens: 0, avgQualityDelivered: 1 };
|
tierResults['enterprise'] = { demandTokens: 0, servedTokens: 0, queuedTokens: 0, rejectedTokens: 0, degradedTokens: 0, avgQualityDelivered: 1 };
|
||||||
@@ -390,34 +456,50 @@ export function processServingPipeline(input: ServingPipelineInput): ServingPipe
|
|||||||
updatedBatchApi.revenue = batchRevenue;
|
updatedBatchApi.revenue = batchRevenue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalServed = Object.values(tierResults).reduce((s, t) => s + t.servedTokens, 0);
|
let totalServed = 0;
|
||||||
const totalQueued = Object.values(tierResults).reduce((s, t) => s + t.queuedTokens, 0);
|
let totalQueued = 0;
|
||||||
const totalRejected = Object.values(tierResults).reduce((s, t) => s + t.rejectedTokens, 0);
|
let totalRejected = 0;
|
||||||
const totalDegraded = Object.values(tierResults).reduce((s, t) => s + t.degradedTokens, 0);
|
let totalDegraded = 0;
|
||||||
|
let qualitySum = 0;
|
||||||
let effectiveQuality = modelQuality;
|
for (const tier of effectivePriorityOrder) {
|
||||||
if (totalServed > 0) {
|
const t = tierResults[tier];
|
||||||
let qualitySum = 0;
|
if (!t) continue;
|
||||||
for (const t of Object.values(tierResults)) {
|
totalServed += t.servedTokens;
|
||||||
qualitySum += t.avgQualityDelivered * t.servedTokens;
|
totalQueued += t.queuedTokens;
|
||||||
}
|
totalRejected += t.rejectedTokens;
|
||||||
effectiveQuality = qualitySum / totalServed;
|
totalDegraded += t.degradedTokens;
|
||||||
|
qualitySum += t.avgQualityDelivered * t.servedTokens;
|
||||||
}
|
}
|
||||||
|
const effectiveQuality = totalServed > 0 ? qualitySum / totalServed : modelQuality;
|
||||||
|
|
||||||
const queuedFraction = totalDemand > 0 ? totalQueued / totalDemand : 0;
|
const queuedFraction = totalDemand > 0 ? totalQueued / totalDemand : 0;
|
||||||
const avgLatencyMs = BASE_LATENCY_MS + queuedFraction * 100 * QUEUE_LATENCY_MS_PER_PERCENT;
|
const avgLatencyMs = BASE_LATENCY_MS + queuedFraction * 100 * QUEUE_LATENCY_MS_PER_PERCENT;
|
||||||
|
|
||||||
const modelUtilization: ModelUtilizationEntry[] = fleet.map(slot => ({
|
cachedUtilization.length = fleet.length;
|
||||||
modelId: slot.modelId,
|
for (let i = 0; i < fleet.length; i++) {
|
||||||
modelName: slot.modelName,
|
const slot = fleet[i];
|
||||||
quantization: slot.quantization,
|
const used = fleetState.used.get(slot.modelId) ?? 0;
|
||||||
qualityScore: slot.qualityScore,
|
const existing = cachedUtilization[i];
|
||||||
throughputCapacity: slot.throughputCapacity,
|
if (existing) {
|
||||||
throughputUsed: fleetState.used.get(slot.modelId) ?? 0,
|
existing.modelId = slot.modelId;
|
||||||
utilization: slot.throughputCapacity > 0
|
existing.modelName = slot.modelName;
|
||||||
? Math.min(1, (fleetState.used.get(slot.modelId) ?? 0) / slot.throughputCapacity)
|
existing.quantization = slot.quantization;
|
||||||
: 0,
|
existing.qualityScore = slot.qualityScore;
|
||||||
}));
|
existing.throughputCapacity = slot.throughputCapacity;
|
||||||
|
existing.throughputUsed = used;
|
||||||
|
existing.utilization = slot.throughputCapacity > 0 ? Math.min(1, used / slot.throughputCapacity) : 0;
|
||||||
|
} else {
|
||||||
|
cachedUtilization[i] = {
|
||||||
|
modelId: slot.modelId,
|
||||||
|
modelName: slot.modelName,
|
||||||
|
quantization: slot.quantization,
|
||||||
|
qualityScore: slot.qualityScore,
|
||||||
|
throughputCapacity: slot.throughputCapacity,
|
||||||
|
throughputUsed: used,
|
||||||
|
utilization: slot.throughputCapacity > 0 ? Math.min(1, used / slot.throughputCapacity) : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const autoScaleBoost = researchUnlocks.autoScalingBonus;
|
const autoScaleBoost = researchUnlocks.autoScalingBonus;
|
||||||
if (autoScaleBoost > 0) {
|
if (autoScaleBoost > 0) {
|
||||||
@@ -443,7 +525,7 @@ export function processServingPipeline(input: ServingPipelineInput): ServingPipe
|
|||||||
totalDegraded,
|
totalDegraded,
|
||||||
effectiveQuality,
|
effectiveQuality,
|
||||||
avgLatencyMs,
|
avgLatencyMs,
|
||||||
modelUtilization,
|
modelUtilization: cachedUtilization,
|
||||||
batchApiTokensServed: batchTokensServed,
|
batchApiTokensServed: batchTokensServed,
|
||||||
batchApiRevenue: batchRevenue,
|
batchApiRevenue: batchRevenue,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import type {
|
import type {
|
||||||
GameState, ModelsState, BaseModel, ModelCapabilities, SafetyProfile,
|
GameState, ModelsState, BaseModel, ModelCapabilities, SafetyProfile,
|
||||||
TrainingPipeline, TrainingEvent, TrainingEventType,
|
TrainingPipeline, TrainingEvent, TrainingEventType,
|
||||||
ModelVariant, VariantCreationJob, EvalJob, BenchmarkResult,
|
ModelVariant, VariantCreationJob,
|
||||||
BenchmarkDefinition,
|
|
||||||
} from '@ai-tycoon/shared';
|
} from '@ai-tycoon/shared';
|
||||||
import { BENCHMARKS } from '../data/benchmarks';
|
|
||||||
import {
|
import {
|
||||||
uuid, VRAM_REQUIREMENTS_BY_GENERATION,
|
uuid, VRAM_REQUIREMENTS_BY_GENERATION,
|
||||||
MOE_CAPABILITY_MULTIPLIER, MOE_SPEED_MULTIPLIER,
|
MOE_CAPABILITY_MULTIPLIER, MOE_SPEED_MULTIPLIER,
|
||||||
@@ -154,14 +152,21 @@ export function processModels(state: GameState, researchBonuses?: ResearchBonuse
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedEvalJobs = processEvalJobs(state);
|
const bestDeployedCapabilities: ModelCapabilities = {
|
||||||
|
reasoning: 0, coding: 0, creative: 0, math: 0,
|
||||||
|
knowledge: 0, multimodal: 0, agents: 0, speed: 0, contextUtilization: 0,
|
||||||
|
};
|
||||||
let bestDeployedModelScore = 0;
|
let bestDeployedModelScore = 0;
|
||||||
let bestDeployedSafetyScore = 0;
|
let bestDeployedSafetyScore = 0;
|
||||||
for (const m of baseModels) {
|
for (const m of baseModels) {
|
||||||
if (!m.isDeployed) continue;
|
if (!m.isDeployed) continue;
|
||||||
if (m.rawCapability > bestDeployedModelScore) bestDeployedModelScore = m.rawCapability;
|
if (m.rawCapability > bestDeployedModelScore) bestDeployedModelScore = m.rawCapability;
|
||||||
if (m.safetyProfile.overallSafety > bestDeployedSafetyScore) bestDeployedSafetyScore = m.safetyProfile.overallSafety;
|
if (m.safetyProfile.overallSafety > bestDeployedSafetyScore) bestDeployedSafetyScore = m.safetyProfile.overallSafety;
|
||||||
|
for (const key of Object.keys(bestDeployedCapabilities) as (keyof ModelCapabilities)[]) {
|
||||||
|
if ((m.capabilities[key] ?? 0) > bestDeployedCapabilities[key]) {
|
||||||
|
bestDeployedCapabilities[key] = m.capabilities[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const f of families) {
|
for (const f of families) {
|
||||||
for (const v of f.variants) {
|
for (const v of f.variants) {
|
||||||
@@ -169,6 +174,11 @@ export function processModels(state: GameState, researchBonuses?: ResearchBonuse
|
|||||||
const score = computeVariantScore(v);
|
const score = computeVariantScore(v);
|
||||||
if (score > bestDeployedModelScore) bestDeployedModelScore = score;
|
if (score > bestDeployedModelScore) bestDeployedModelScore = score;
|
||||||
if (v.safetyProfile.overallSafety > bestDeployedSafetyScore) bestDeployedSafetyScore = v.safetyProfile.overallSafety;
|
if (v.safetyProfile.overallSafety > bestDeployedSafetyScore) bestDeployedSafetyScore = v.safetyProfile.overallSafety;
|
||||||
|
for (const key of Object.keys(bestDeployedCapabilities) as (keyof ModelCapabilities)[]) {
|
||||||
|
if ((v.capabilities[key] ?? 0) > bestDeployedCapabilities[key]) {
|
||||||
|
bestDeployedCapabilities[key] = v.capabilities[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,10 +189,9 @@ export function processModels(state: GameState, researchBonuses?: ResearchBonuse
|
|||||||
families,
|
families,
|
||||||
activeTrainingPipelines: updatedPipelines,
|
activeTrainingPipelines: updatedPipelines,
|
||||||
variantJobs: updatedVariantJobs.jobs,
|
variantJobs: updatedVariantJobs.jobs,
|
||||||
evalJobs: updatedEvalJobs.jobs,
|
|
||||||
benchmarkResults: [...state.models.benchmarkResults, ...updatedEvalJobs.newResults],
|
|
||||||
bestDeployedModelScore,
|
bestDeployedModelScore,
|
||||||
bestDeployedSafetyScore,
|
bestDeployedSafetyScore,
|
||||||
|
bestDeployedCapabilities,
|
||||||
},
|
},
|
||||||
completedModels,
|
completedModels,
|
||||||
notifications,
|
notifications,
|
||||||
@@ -490,47 +499,6 @@ function createVariant(job: VariantCreationJob, base: BaseModel): ModelVariant {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function processEvalJobs(state: GameState): { jobs: EvalJob[]; newResults: BenchmarkResult[] } {
|
|
||||||
const newResults: BenchmarkResult[] = [];
|
|
||||||
const allModels: (BaseModel | ModelVariant)[] = [
|
|
||||||
...state.models.baseModels,
|
|
||||||
...state.models.families.flatMap(f => f.variants),
|
|
||||||
];
|
|
||||||
|
|
||||||
const jobs = state.models.evalJobs.map(job => {
|
|
||||||
if (job.status !== 'active') return job;
|
|
||||||
const newProgress = job.progressTicks + 1;
|
|
||||||
if (newProgress >= job.totalTicks) {
|
|
||||||
const model = allModels.find(m => m.id === job.modelId);
|
|
||||||
if (model) {
|
|
||||||
const results = computeBenchmarkScores(model, job.benchmarkIds, state.meta.tickCount);
|
|
||||||
newResults.push(...results);
|
|
||||||
return { ...job, status: 'completed' as const, progressTicks: job.totalTicks, results };
|
|
||||||
}
|
|
||||||
return { ...job, status: 'completed' as const, progressTicks: job.totalTicks };
|
|
||||||
}
|
|
||||||
return { ...job, progressTicks: newProgress };
|
|
||||||
});
|
|
||||||
return { jobs, newResults };
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeBenchmarkScores(
|
|
||||||
model: BaseModel | ModelVariant,
|
|
||||||
benchmarkIds: string[],
|
|
||||||
tick: number,
|
|
||||||
): BenchmarkResult[] {
|
|
||||||
const benchmarkMap = new Map(BENCHMARKS.map(b => [b.id, b]));
|
|
||||||
return benchmarkIds.map(id => {
|
|
||||||
const bench = benchmarkMap.get(id);
|
|
||||||
if (!bench) return { benchmarkId: id, modelId: model.id, score: 0, ranAtTick: tick };
|
|
||||||
const primary = model.capabilities[bench.primaryCapability] ?? 0;
|
|
||||||
const secondary = bench.secondaryCapability ? (model.capabilities[bench.secondaryCapability] ?? 0) : 0;
|
|
||||||
const noise = (Math.random() - 0.5) * 6;
|
|
||||||
const score = clamp(primary * 0.7 + secondary * 0.3 + noise);
|
|
||||||
return { benchmarkId: id, modelId: model.id, score, ranAtTick: tick };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeVariantScore(variant: ModelVariant): number {
|
function computeVariantScore(variant: ModelVariant): number {
|
||||||
const c = variant.capabilities;
|
const c = variant.capabilities;
|
||||||
return (c.reasoning * 0.25 + c.coding * 0.2 + c.creative * 0.15 + c.math * 0.15 + c.knowledge * 0.15 + c.agents * 0.1);
|
return (c.reasoning * 0.25 + c.coding * 0.2 + c.creative * 0.15 + c.math * 0.15 + c.knowledge * 0.15 + c.agents * 0.1);
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { processTick, setAchievementDefinitions } from './tick';
|
|||||||
import { createTestState, createSeededRNG } from './__test-utils__';
|
import { createTestState, createSeededRNG } from './__test-utils__';
|
||||||
import { ACHIEVEMENT_DEFINITIONS } from './data/achievements';
|
import { ACHIEVEMENT_DEFINITIONS } from './data/achievements';
|
||||||
import { resetResearchBonusCache } from './systems/researchBonuses';
|
import { resetResearchBonusCache } from './systems/researchBonuses';
|
||||||
|
import { resetFleetCache } from './systems/market/servingPipeline';
|
||||||
|
|
||||||
const rng = createSeededRNG(42);
|
const rng = createSeededRNG(42);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
rng.install();
|
rng.install();
|
||||||
resetResearchBonusCache();
|
resetResearchBonusCache();
|
||||||
|
resetFleetCache();
|
||||||
setAchievementDefinitions(ACHIEVEMENT_DEFINITIONS);
|
setAchievementDefinitions(ACHIEVEMENT_DEFINITIONS);
|
||||||
});
|
});
|
||||||
afterEach(() => rng.uninstall());
|
afterEach(() => rng.uninstall());
|
||||||
@@ -66,7 +68,6 @@ describe('processTick', () => {
|
|||||||
isDeployed: true, trainedAtTick: 0, trainingCostTotal: 0, trainingStagesCompleted: ['pretraining' as const],
|
isDeployed: true, trainedAtTick: 0, trainingCostTotal: 0, trainingStagesCompleted: ['pretraining' as const],
|
||||||
sizeTier: 'small' as const, version: 1.0, sftSpecializations: ['general' as const], alignmentMethod: 'rlhf' as const,
|
sizeTier: 'small' as const, version: 1.0, sftSpecializations: ['general' as const], alignmentMethod: 'rlhf' as const,
|
||||||
dataMix: { web: 0.4, code: 0.2, books: 0.15, academic: 0.1, conversational: 0.1, specialized: 0.05 },
|
dataMix: { web: 0.4, code: 0.2, books: 0.15, academic: 0.1, conversational: 0.1, specialized: 0.05 },
|
||||||
benchmarkResults: {},
|
|
||||||
};
|
};
|
||||||
const state = createTestState({
|
const state = createTestState({
|
||||||
meta: { currentEra: 'startup' },
|
meta: { currentEra: 'startup' },
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export function deployModel(state: GameState, modelId: string): boolean {
|
|||||||
if (!model) return false;
|
if (!model) return false;
|
||||||
|
|
||||||
model.isDeployed = true;
|
model.isDeployed = true;
|
||||||
|
state.models.deploymentVersion = (state.models.deploymentVersion ?? 0) + 1;
|
||||||
|
|
||||||
for (const pl of state.models.productLines) {
|
for (const pl of state.models.productLines) {
|
||||||
pl.modelId = modelId;
|
pl.modelId = modelId;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { GameState } from '@ai-tycoon/shared';
|
import type { GameState } from '@ai-tycoon/shared';
|
||||||
import { processTick, setAchievementDefinitions, ACHIEVEMENT_DEFINITIONS, resetResearchBonusCache } from '@ai-tycoon/game-engine';
|
import { processTick, setAchievementDefinitions, ACHIEVEMENT_DEFINITIONS, resetResearchBonusCache, resetFleetCache } from '@ai-tycoon/game-engine';
|
||||||
import type { TickNotification } from '@ai-tycoon/game-engine';
|
import type { TickNotification } from '@ai-tycoon/game-engine';
|
||||||
import type { Strategy, SimulationMetrics } from './strategies/types';
|
import type { Strategy, SimulationMetrics } from './strategies/types';
|
||||||
import { collectMetrics } from './analysis/metrics';
|
import { collectMetrics } from './analysis/metrics';
|
||||||
@@ -78,6 +78,7 @@ export function runSimulation(config: SimulationConfig): SimulationResult {
|
|||||||
|
|
||||||
resetIds();
|
resetIds();
|
||||||
resetResearchBonusCache();
|
resetResearchBonusCache();
|
||||||
|
resetFleetCache();
|
||||||
|
|
||||||
let rng: ReturnType<typeof createSeededRNG> | null = null;
|
let rng: ReturnType<typeof createSeededRNG> | null = null;
|
||||||
if (config.seed !== undefined) {
|
if (config.seed !== undefined) {
|
||||||
|
|||||||
@@ -91,24 +91,6 @@ export interface DataCenter {
|
|||||||
// --- Network Topology (6-Tier Clos) ---
|
// --- Network Topology (6-Tier Clos) ---
|
||||||
|
|
||||||
export type SwitchTier = 'tor' | 't1' | 't2' | 't3' | 't4' | 't5';
|
export type SwitchTier = 'tor' | 't1' | 't2' | 't3' | 't4' | 't5';
|
||||||
export type SwitchStatus = 'healthy' | 'failed' | 'repairing';
|
|
||||||
|
|
||||||
export interface NetworkSwitch {
|
|
||||||
id: string;
|
|
||||||
tier: SwitchTier;
|
|
||||||
status: SwitchStatus;
|
|
||||||
dcId: string | null;
|
|
||||||
campusId: string | null;
|
|
||||||
clusterId: string | null;
|
|
||||||
uplinkIds: string[];
|
|
||||||
downlinkIds: string[];
|
|
||||||
activeUplinks: number;
|
|
||||||
totalUplinks: number;
|
|
||||||
effectiveBandwidth: number;
|
|
||||||
repairProgress: number;
|
|
||||||
repairTotal: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SwitchTierConfig {
|
export interface SwitchTierConfig {
|
||||||
tier: SwitchTier;
|
tier: SwitchTier;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -121,11 +103,17 @@ export interface SwitchTierConfig {
|
|||||||
powerDrawKW: number;
|
powerDrawKW: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RepairBatch {
|
||||||
|
tier: SwitchTier;
|
||||||
|
count: number;
|
||||||
|
ticksRemaining: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DCNetworkSummary {
|
export interface DCNetworkSummary {
|
||||||
switchIds: string[];
|
|
||||||
networkRackCount: number;
|
|
||||||
totalByTier: Partial<Record<SwitchTier, number>>;
|
totalByTier: Partial<Record<SwitchTier, number>>;
|
||||||
healthyByTier: Partial<Record<SwitchTier, number>>;
|
healthyByTier: Partial<Record<SwitchTier, number>>;
|
||||||
|
repairBatches: RepairBatch[];
|
||||||
|
networkRackCount: number;
|
||||||
racksDisconnected: number;
|
racksDisconnected: number;
|
||||||
racksDegraded: number;
|
racksDegraded: number;
|
||||||
averageBandwidth: number;
|
averageBandwidth: number;
|
||||||
@@ -133,14 +121,12 @@ export interface DCNetworkSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CampusNetworkSummary {
|
export interface CampusNetworkSummary {
|
||||||
switchIds: string[];
|
|
||||||
totalT4: number;
|
totalT4: number;
|
||||||
healthyT4: number;
|
healthyT4: number;
|
||||||
crossDCBandwidth: number;
|
crossDCBandwidth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClusterNetworkSummary {
|
export interface ClusterNetworkSummary {
|
||||||
switchIds: string[];
|
|
||||||
totalT5: number;
|
totalT5: number;
|
||||||
healthyT5: number;
|
healthyT5: number;
|
||||||
crossCampusBandwidth: number;
|
crossCampusBandwidth: number;
|
||||||
@@ -262,7 +248,6 @@ export interface ClusterCostConfig {
|
|||||||
|
|
||||||
export interface InfrastructureState {
|
export interface InfrastructureState {
|
||||||
clusters: Cluster[];
|
clusters: Cluster[];
|
||||||
switchRegistry: Record<string, NetworkSwitch>;
|
|
||||||
totalFlops: number;
|
totalFlops: number;
|
||||||
totalTrainingFlops: number;
|
totalTrainingFlops: number;
|
||||||
totalInferenceFlops: number;
|
totalInferenceFlops: number;
|
||||||
@@ -276,7 +261,6 @@ export interface InfrastructureState {
|
|||||||
|
|
||||||
export const INITIAL_INFRASTRUCTURE: InfrastructureState = {
|
export const INITIAL_INFRASTRUCTURE: InfrastructureState = {
|
||||||
clusters: [],
|
clusters: [],
|
||||||
switchRegistry: {},
|
|
||||||
totalFlops: 0,
|
totalFlops: 0,
|
||||||
totalTrainingFlops: 0,
|
totalTrainingFlops: 0,
|
||||||
totalInferenceFlops: 0,
|
totalInferenceFlops: 0,
|
||||||
|
|||||||
@@ -182,45 +182,6 @@ export interface QuantizationConfig {
|
|||||||
variantName: string;
|
variantName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BenchmarkCategory = 'reasoning' | 'coding' | 'math' | 'knowledge' | 'safety' | 'chat' | 'multimodal' | 'agents';
|
|
||||||
|
|
||||||
export interface BenchmarkDefinition {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
category: BenchmarkCategory;
|
|
||||||
description: string;
|
|
||||||
primaryCapability: keyof ModelCapabilities;
|
|
||||||
secondaryCapability?: keyof ModelCapabilities;
|
|
||||||
computeCost: number;
|
|
||||||
ticksToRun: number;
|
|
||||||
unlockedAtEra: Era;
|
|
||||||
marketRelevance: {
|
|
||||||
consumer: number;
|
|
||||||
enterprise: number;
|
|
||||||
developer: number;
|
|
||||||
research: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BenchmarkResult {
|
|
||||||
benchmarkId: string;
|
|
||||||
modelId: string;
|
|
||||||
score: number;
|
|
||||||
ranAtTick: number;
|
|
||||||
rank?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EvalJob {
|
|
||||||
id: string;
|
|
||||||
modelId: string;
|
|
||||||
benchmarkIds: string[];
|
|
||||||
progressTicks: number;
|
|
||||||
totalTicks: number;
|
|
||||||
computeAllocated: number;
|
|
||||||
status: 'active' | 'completed';
|
|
||||||
results: BenchmarkResult[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ProductLineType = 'text-api' | 'chat-product' | 'chat-free' | 'chat-enterprise' | 'code-api' | 'image' | 'agents-api';
|
export type ProductLineType = 'text-api' | 'chat-product' | 'chat-free' | 'chat-enterprise' | 'code-api' | 'image' | 'agents-api';
|
||||||
|
|
||||||
export interface ProductPricing {
|
export interface ProductPricing {
|
||||||
@@ -246,11 +207,11 @@ export interface ModelsState {
|
|||||||
baseModels: BaseModel[];
|
baseModels: BaseModel[];
|
||||||
activeTrainingPipelines: TrainingPipeline[];
|
activeTrainingPipelines: TrainingPipeline[];
|
||||||
variantJobs: VariantCreationJob[];
|
variantJobs: VariantCreationJob[];
|
||||||
evalJobs: EvalJob[];
|
|
||||||
benchmarkResults: BenchmarkResult[];
|
|
||||||
productLines: ProductLine[];
|
productLines: ProductLine[];
|
||||||
bestDeployedModelScore: number;
|
bestDeployedModelScore: number;
|
||||||
bestDeployedSafetyScore: number;
|
bestDeployedSafetyScore: number;
|
||||||
|
bestDeployedCapabilities: ModelCapabilities;
|
||||||
|
deploymentVersion: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_DATA_MIX: DataMixAllocation = {
|
export const DEFAULT_DATA_MIX: DataMixAllocation = {
|
||||||
@@ -271,8 +232,6 @@ export const INITIAL_MODELS: ModelsState = {
|
|||||||
baseModels: [],
|
baseModels: [],
|
||||||
activeTrainingPipelines: [],
|
activeTrainingPipelines: [],
|
||||||
variantJobs: [],
|
variantJobs: [],
|
||||||
evalJobs: [],
|
|
||||||
benchmarkResults: [],
|
|
||||||
productLines: [
|
productLines: [
|
||||||
{
|
{
|
||||||
id: 'text-api',
|
id: 'text-api',
|
||||||
@@ -307,4 +266,6 @@ export const INITIAL_MODELS: ModelsState = {
|
|||||||
],
|
],
|
||||||
bestDeployedModelScore: 0,
|
bestDeployedModelScore: 0,
|
||||||
bestDeployedSafetyScore: 0,
|
bestDeployedSafetyScore: 0,
|
||||||
|
bestDeployedCapabilities: { reasoning: 0, coding: 0, creative: 0, math: 0, knowledge: 0, multimodal: 0, agents: 0, speed: 0, contextUtilization: 0 },
|
||||||
|
deploymentVersion: 0,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user