345 lines
12 KiB
TypeScript
345 lines
12 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { createTestState } from '../__test-utils__';
|
|
import { computeCapacity, finalizeCompute } from './computeSystem';
|
|
import type { InfrastructureState } from '@ai-tycoon/shared';
|
|
import { INITIAL_INFRASTRUCTURE, FLOPS_TO_TOKENS_MULTIPLIER, COMPUTE_SNAPSHOT_INTERVAL, MAX_COMPUTE_HISTORY } from '@ai-tycoon/shared';
|
|
|
|
function createInfrastructure(overrides: Partial<InfrastructureState> = {}): InfrastructureState {
|
|
return { ...INITIAL_INFRASTRUCTURE, ...overrides };
|
|
}
|
|
|
|
describe('computeCapacity', () => {
|
|
it('splits allocation between training and inference', () => {
|
|
const state = createTestState({
|
|
compute: { trainingAllocation: 0.7 },
|
|
});
|
|
const infra = createInfrastructure({
|
|
totalTrainingFlops: 1000,
|
|
totalInferenceFlops: 500,
|
|
});
|
|
|
|
const result = computeCapacity(state, infra);
|
|
|
|
expect(result.trainingAllocation).toBe(0.7);
|
|
expect(result.inferenceAllocation).toBeCloseTo(0.3);
|
|
});
|
|
|
|
it('calculates effectiveTrainingFlops with cross-hardware contribution', () => {
|
|
const state = createTestState({
|
|
compute: { trainingAllocation: 0.6 },
|
|
});
|
|
const infra = createInfrastructure({
|
|
totalTrainingFlops: 1000,
|
|
totalInferenceFlops: 400,
|
|
});
|
|
|
|
const result = computeCapacity(state, infra);
|
|
|
|
// effectiveTraining = 1000 * 0.6 + 400 * 0.6 * 0.3 = 600 + 72 = 672
|
|
expect(result.effectiveTrainingFlops).toBeCloseTo(672);
|
|
});
|
|
|
|
it('calculates effectiveInferenceFlops with cross-hardware contribution', () => {
|
|
const state = createTestState({
|
|
compute: { trainingAllocation: 0.6 },
|
|
});
|
|
const infra = createInfrastructure({
|
|
totalTrainingFlops: 1000,
|
|
totalInferenceFlops: 400,
|
|
});
|
|
|
|
const result = computeCapacity(state, infra);
|
|
|
|
// inferenceAlloc = 0.4
|
|
// effectiveInference = (400 * 0.4 + 1000 * 0.4 * 0.5) * 1 = (160 + 200) * 1 = 360
|
|
expect(result.effectiveInferenceFlops).toBeCloseTo(360);
|
|
});
|
|
|
|
it('applies research bonuses to inference calculation', () => {
|
|
const state = createTestState({
|
|
compute: { trainingAllocation: 0.5 },
|
|
});
|
|
const infra = createInfrastructure({
|
|
totalTrainingFlops: 1000,
|
|
totalInferenceFlops: 1000,
|
|
});
|
|
const bonuses = {
|
|
tokensPerFlopBonus: 0.3,
|
|
inferenceEfficiencyBonus: 0.15,
|
|
energyCostReduction: 0,
|
|
pipelineSpeedBonus: 0,
|
|
trainingSpeedBonus: 0,
|
|
dataQualityBonus: 0,
|
|
sdkCoverageBonus: 0,
|
|
globalCapabilityBonus: 0,
|
|
reasoningBonus: 0,
|
|
codingBonus: 0,
|
|
creativeBonus: 0,
|
|
multimodalBonus: 0,
|
|
agentsBonus: 0,
|
|
reputationBonus: 0,
|
|
safetyBonus: 0,
|
|
autoScalingBonus: 0,
|
|
};
|
|
|
|
const result = computeCapacity(state, infra, bonuses);
|
|
|
|
// inferenceBoost = 1 + 0.3 + 0.15 = 1.45
|
|
// effectiveInference = (1000 * 0.5 + 1000 * 0.5 * 0.5) * 1.45 = (500 + 250) * 1.45 = 1087.5
|
|
expect(result.effectiveInferenceFlops).toBeCloseTo(1087.5);
|
|
});
|
|
|
|
it('does not apply research bonuses to training calculation', () => {
|
|
const state = createTestState({
|
|
compute: { trainingAllocation: 0.5 },
|
|
});
|
|
const infra = createInfrastructure({
|
|
totalTrainingFlops: 1000,
|
|
totalInferenceFlops: 1000,
|
|
});
|
|
const bonuses = {
|
|
tokensPerFlopBonus: 0.5,
|
|
inferenceEfficiencyBonus: 0.5,
|
|
energyCostReduction: 0,
|
|
pipelineSpeedBonus: 0,
|
|
trainingSpeedBonus: 0,
|
|
dataQualityBonus: 0,
|
|
sdkCoverageBonus: 0,
|
|
globalCapabilityBonus: 0,
|
|
reasoningBonus: 0,
|
|
codingBonus: 0,
|
|
creativeBonus: 0,
|
|
multimodalBonus: 0,
|
|
agentsBonus: 0,
|
|
reputationBonus: 0,
|
|
safetyBonus: 0,
|
|
autoScalingBonus: 0,
|
|
};
|
|
|
|
const result = computeCapacity(state, infra, bonuses);
|
|
|
|
// effectiveTraining = 1000 * 0.5 + 1000 * 0.5 * 0.3 = 500 + 150 = 650
|
|
// (same with or without bonuses)
|
|
expect(result.effectiveTrainingFlops).toBeCloseTo(650);
|
|
});
|
|
|
|
it('calculates tokensPerSecondCapacity from effectiveInferenceFlops', () => {
|
|
const state = createTestState({
|
|
compute: { trainingAllocation: 0.0 },
|
|
});
|
|
const infra = createInfrastructure({
|
|
totalTrainingFlops: 0,
|
|
totalInferenceFlops: 100,
|
|
});
|
|
|
|
const result = computeCapacity(state, infra);
|
|
|
|
// inferenceAlloc = 1.0
|
|
// effectiveInference = (100 * 1.0 + 0 * 1.0 * 0.5) * 1 = 100
|
|
// tokensPerSecond = 100 * 26 = 2600
|
|
expect(result.tokensPerSecondCapacity).toBe(100 * FLOPS_TO_TOKENS_MULTIPLIER);
|
|
});
|
|
|
|
it('reports totalFlops as sum of training and inference', () => {
|
|
const state = createTestState();
|
|
const infra = createInfrastructure({
|
|
totalTrainingFlops: 750,
|
|
totalInferenceFlops: 250,
|
|
});
|
|
|
|
const result = computeCapacity(state, infra);
|
|
|
|
expect(result.totalFlops).toBe(1000);
|
|
});
|
|
|
|
it('passes through totalVramGB from infrastructure', () => {
|
|
const state = createTestState();
|
|
const infra = createInfrastructure({
|
|
totalVramGB: 512,
|
|
});
|
|
|
|
const result = computeCapacity(state, infra);
|
|
|
|
expect(result.totalVramGB).toBe(512);
|
|
});
|
|
|
|
it('returns all zeros when infrastructure has no hardware', () => {
|
|
const state = createTestState({
|
|
compute: { trainingAllocation: 0.5 },
|
|
});
|
|
const infra = createInfrastructure({
|
|
totalTrainingFlops: 0,
|
|
totalInferenceFlops: 0,
|
|
totalVramGB: 0,
|
|
});
|
|
|
|
const result = computeCapacity(state, infra);
|
|
|
|
expect(result.totalFlops).toBe(0);
|
|
expect(result.effectiveTrainingFlops).toBe(0);
|
|
expect(result.effectiveInferenceFlops).toBe(0);
|
|
expect(result.tokensPerSecondCapacity).toBe(0);
|
|
});
|
|
|
|
it('handles full training allocation (1.0)', () => {
|
|
const state = createTestState({
|
|
compute: { trainingAllocation: 1.0 },
|
|
});
|
|
const infra = createInfrastructure({
|
|
totalTrainingFlops: 1000,
|
|
totalInferenceFlops: 500,
|
|
});
|
|
|
|
const result = computeCapacity(state, infra);
|
|
|
|
// effectiveTraining = 1000 * 1.0 + 500 * 1.0 * 0.3 = 1000 + 150 = 1150
|
|
expect(result.effectiveTrainingFlops).toBeCloseTo(1150);
|
|
// inferenceAlloc = 0, so effectiveInference = 0
|
|
expect(result.effectiveInferenceFlops).toBe(0);
|
|
expect(result.tokensPerSecondCapacity).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('finalizeCompute', () => {
|
|
it('calculates inferenceUtilization as demand / capacity', () => {
|
|
const capacity = computeCapacity(
|
|
createTestState({ compute: { trainingAllocation: 0 } }),
|
|
createInfrastructure({ totalInferenceFlops: 100 }),
|
|
);
|
|
// tokensPerSecondCapacity = 100 * 26 = 2600
|
|
|
|
const result = finalizeCompute(capacity, 1300, [], 1);
|
|
|
|
expect(result.inferenceUtilization).toBeCloseTo(0.5);
|
|
});
|
|
|
|
it('clamps utilization to 1 when demand exceeds capacity', () => {
|
|
const capacity = computeCapacity(
|
|
createTestState({ compute: { trainingAllocation: 0 } }),
|
|
createInfrastructure({ totalInferenceFlops: 100 }),
|
|
);
|
|
|
|
const result = finalizeCompute(capacity, 999999, [], 1);
|
|
|
|
expect(result.inferenceUtilization).toBe(1);
|
|
});
|
|
|
|
it('sets utilization to 1 when capacity is 0 but demand > 0', () => {
|
|
const capacity = computeCapacity(
|
|
createTestState({ compute: { trainingAllocation: 0.5 } }),
|
|
createInfrastructure({ totalTrainingFlops: 0, totalInferenceFlops: 0 }),
|
|
);
|
|
|
|
const result = finalizeCompute(capacity, 100, [], 1);
|
|
|
|
expect(result.inferenceUtilization).toBe(1);
|
|
});
|
|
|
|
it('sets utilization to 0 when both capacity and demand are 0', () => {
|
|
const capacity = computeCapacity(
|
|
createTestState({ compute: { trainingAllocation: 0.5 } }),
|
|
createInfrastructure({ totalTrainingFlops: 0, totalInferenceFlops: 0 }),
|
|
);
|
|
|
|
const result = finalizeCompute(capacity, 0, [], 1);
|
|
|
|
expect(result.inferenceUtilization).toBe(0);
|
|
});
|
|
|
|
it('adds a history snapshot when tickCount is a multiple of COMPUTE_SNAPSHOT_INTERVAL', () => {
|
|
const capacity = computeCapacity(
|
|
createTestState({ compute: { trainingAllocation: 0.5 } }),
|
|
createInfrastructure({ totalTrainingFlops: 500, totalInferenceFlops: 500 }),
|
|
);
|
|
|
|
const tick = COMPUTE_SNAPSHOT_INTERVAL; // 60
|
|
const result = finalizeCompute(capacity, 200, [], tick);
|
|
|
|
expect(result.computeHistory).toHaveLength(1);
|
|
expect(result.computeHistory[0].tick).toBe(tick);
|
|
expect(result.computeHistory[0].totalFlops).toBe(capacity.totalFlops);
|
|
expect(result.computeHistory[0].tokensPerSecondDemand).toBe(200);
|
|
});
|
|
|
|
it('does not add a snapshot when tickCount is not a multiple of COMPUTE_SNAPSHOT_INTERVAL', () => {
|
|
const capacity = computeCapacity(
|
|
createTestState({ compute: { trainingAllocation: 0.5 } }),
|
|
createInfrastructure({ totalTrainingFlops: 500, totalInferenceFlops: 500 }),
|
|
);
|
|
|
|
const result = finalizeCompute(capacity, 200, [], 1);
|
|
|
|
expect(result.computeHistory).toHaveLength(0);
|
|
});
|
|
|
|
it('preserves existing history entries', () => {
|
|
const capacity = computeCapacity(
|
|
createTestState({ compute: { trainingAllocation: 0.5 } }),
|
|
createInfrastructure({ totalTrainingFlops: 500, totalInferenceFlops: 500 }),
|
|
);
|
|
|
|
const existingHistory = [
|
|
{
|
|
tick: 60,
|
|
totalFlops: 800,
|
|
effectiveTrainingFlops: 400,
|
|
effectiveInferenceFlops: 400,
|
|
inferenceUtilization: 0.5,
|
|
tokensPerSecondCapacity: 10400,
|
|
tokensPerSecondDemand: 5200,
|
|
},
|
|
];
|
|
|
|
const result = finalizeCompute(capacity, 200, existingHistory, COMPUTE_SNAPSHOT_INTERVAL * 2);
|
|
|
|
expect(result.computeHistory).toHaveLength(2);
|
|
expect(result.computeHistory[0].tick).toBe(60);
|
|
expect(result.computeHistory[1].tick).toBe(COMPUTE_SNAPSHOT_INTERVAL * 2);
|
|
});
|
|
|
|
it('trims history when it exceeds MAX_COMPUTE_HISTORY', () => {
|
|
const capacity = computeCapacity(
|
|
createTestState({ compute: { trainingAllocation: 0.5 } }),
|
|
createInfrastructure({ totalTrainingFlops: 100, totalInferenceFlops: 100 }),
|
|
);
|
|
|
|
// Create a history that is exactly at the limit
|
|
const fullHistory = Array.from({ length: MAX_COMPUTE_HISTORY }, (_, i) => ({
|
|
tick: (i + 1) * COMPUTE_SNAPSHOT_INTERVAL,
|
|
totalFlops: 200,
|
|
effectiveTrainingFlops: 100,
|
|
effectiveInferenceFlops: 100,
|
|
inferenceUtilization: 0.5,
|
|
tokensPerSecondCapacity: 2600,
|
|
tokensPerSecondDemand: 1300,
|
|
}));
|
|
|
|
const nextSnapshotTick = (MAX_COMPUTE_HISTORY + 1) * COMPUTE_SNAPSHOT_INTERVAL;
|
|
const result = finalizeCompute(capacity, 1300, fullHistory, nextSnapshotTick);
|
|
|
|
expect(result.computeHistory).toHaveLength(MAX_COMPUTE_HISTORY);
|
|
// Oldest entry should have been shifted out
|
|
expect(result.computeHistory[0].tick).toBe(2 * COMPUTE_SNAPSHOT_INTERVAL);
|
|
// Newest entry should be our new snapshot
|
|
expect(result.computeHistory[result.computeHistory.length - 1].tick).toBe(nextSnapshotTick);
|
|
});
|
|
|
|
it('carries capacity fields into the returned ComputeState', () => {
|
|
const capacity = computeCapacity(
|
|
createTestState({ compute: { trainingAllocation: 0.3 } }),
|
|
createInfrastructure({ totalTrainingFlops: 200, totalInferenceFlops: 800, totalVramGB: 256 }),
|
|
);
|
|
|
|
const result = finalizeCompute(capacity, 500, [], 1);
|
|
|
|
expect(result.totalFlops).toBe(capacity.totalFlops);
|
|
expect(result.totalTrainingFlops).toBe(capacity.totalTrainingFlops);
|
|
expect(result.totalInferenceFlops).toBe(capacity.totalInferenceFlops);
|
|
expect(result.totalVramGB).toBe(capacity.totalVramGB);
|
|
expect(result.effectiveTrainingFlops).toBe(capacity.effectiveTrainingFlops);
|
|
expect(result.effectiveInferenceFlops).toBe(capacity.effectiveInferenceFlops);
|
|
expect(result.tokensPerSecondCapacity).toBe(capacity.tokensPerSecondCapacity);
|
|
expect(result.tokensPerSecondDemand).toBe(500);
|
|
});
|
|
});
|