Redesign model lifecycle: upfront SFT/alignment, multi-size families, point releases, quantization-only variants
CI / build-and-push (push) Successful in 45s

Training pipeline now requires SFT specializations and alignment method configured at start — no more
mid-training configuration step. Model families support multiple size tiers (Nano/Small/Medium/Large/Flagship)
trained independently, mimicking real AI company model families. Point releases iterate on deployed models
with 40% training time and 8% capability gain. Distillation and fine-tuning variants removed — players
train smaller size tiers or configure SFT during initial training instead. Only quantization remains as
a variant type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 11:00:38 -04:00
parent 775c6a4fa5
commit d7d77238b9
6 changed files with 530 additions and 600 deletions
+114 -155
View File
@@ -7,8 +7,6 @@ import type {
import { BENCHMARKS } from '../data/benchmarks';
import {
uuid, VRAM_REQUIREMENTS_BY_GENERATION,
SFT_TIME_FRACTION, SFT_COMPUTE_FRACTION,
ALIGNMENT_TIME_FRACTION, ALIGNMENT_COMPUTE_FRACTION,
MOE_CAPABILITY_MULTIPLIER, MOE_SPEED_MULTIPLIER,
EVENT_BASE_PROBABILITY,
LOSS_SPIKE_DELAY_MIN, LOSS_SPIKE_DELAY_MAX,
@@ -18,8 +16,8 @@ import {
ALIGNMENT_METHODS,
SFT_SPECIALIZATION_BONUSES,
QUANTIZATION_CONFIGS,
DISTILLATION_BASE_RETENTION,
QUANTIZATION_TICKS,
POINT_RELEASE_CAPABILITY_GAIN,
SIZE_TIER_LABELS,
} from '@ai-tycoon/shared';
import type { ResearchBonuses } from './researchBonuses';
@@ -101,60 +99,25 @@ export function processModels(state: GameState, researchBonuses?: ResearchBonuse
stage.computeAllocated = effectiveFlops;
stage.lossValue = Math.max(0.01, 10 * Math.exp(-stage.progressTicks / stage.totalTicks * 3));
const progressRatio = stage.progressTicks / stage.totalTicks;
if (progressRatio >= 0.75 && progressRatio < 0.78 && !pipeline.stages.sft && !pipeline.stages.alignment) {
notifications.push({
title: 'Post-Training Reminder',
message: `${pipeline.modelName} is 75% pre-trained. Configure SFT/Alignment now or they'll be skipped!`,
type: 'warning',
action: { label: 'Configure Now', page: 'models', modelsTab: 'overview' },
});
}
if (stage.progressTicks >= stage.totalTicks) {
stage.isComplete = true;
stage.progressTicks = stage.totalTicks;
if (updated.stages.sft) {
updated.currentStage = 'sft';
notifications.push({ title: 'Pre-training Complete', message: `${pipeline.modelName}: Moving to supervised fine-tuning.`, type: 'info' });
} else if (updated.stages.alignment) {
updated.currentStage = 'alignment';
notifications.push({ title: 'Pre-training Complete', message: `${pipeline.modelName}: Moving to alignment.`, type: 'info' });
} else {
const model = createBaseModel(updated, state, researchBonuses);
baseModels = [...baseModels, model];
families = families.map(f =>
f.id === pipeline.familyId ? { ...f, baseModelId: model.id } : f,
);
completedModels.push(model);
updated.status = 'completed';
}
updated.currentStage = 'sft';
notifications.push({ title: 'Pre-training Complete', message: `${pipeline.modelName}: Moving to supervised fine-tuning.`, type: 'info' });
}
updated = { ...updated, stages: { ...updated.stages, pretraining: stage } };
} else if (pipeline.currentStage === 'sft' && pipeline.stages.sft) {
} else if (pipeline.currentStage === 'sft') {
const stage = { ...pipeline.stages.sft };
stage.progressTicks += speedMultiplier;
if (stage.progressTicks >= stage.totalTicks) {
stage.isComplete = true;
stage.progressTicks = stage.totalTicks;
if (updated.stages.alignment) {
updated.currentStage = 'alignment';
notifications.push({ title: 'SFT Complete', message: `${pipeline.modelName}: Moving to alignment.`, type: 'info' });
} else {
const model = createBaseModel(updated, state, researchBonuses);
baseModels = [...baseModels, model];
families = families.map(f =>
f.id === pipeline.familyId ? { ...f, baseModelId: model.id } : f,
);
completedModels.push(model);
updated.status = 'completed';
}
updated.currentStage = 'alignment';
notifications.push({ title: 'SFT Complete', message: `${pipeline.modelName}: Moving to alignment.`, type: 'info' });
}
updated = { ...updated, stages: { ...updated.stages, sft: stage } };
} else if (pipeline.currentStage === 'alignment' && pipeline.stages.alignment) {
} else if (pipeline.currentStage === 'alignment') {
const stage = { ...pipeline.stages.alignment };
stage.progressTicks += speedMultiplier;
@@ -165,7 +128,7 @@ export function processModels(state: GameState, researchBonuses?: ResearchBonuse
const model = createBaseModel(updated, state, researchBonuses);
baseModels = [...baseModels, model];
families = families.map(f =>
f.id === pipeline.familyId ? { ...f, baseModelId: model.id } : f,
f.id === pipeline.familyId ? { ...f, baseModelIds: [...f.baseModelIds, model.id] } : f,
);
completedModels.push(model);
updated.status = 'completed';
@@ -320,79 +283,93 @@ function createBaseModel(
const dataTokens = pipeline.stages.pretraining.targetTokens;
const params = architecture.totalParameters;
// Pillar 1: Parameters (0-30) — larger models have higher ceiling
const paramFactor = Math.min(30, Math.log2(1 + params) * 4.5);
const sourceModel = pipeline.isPointRelease && pipeline.sourceModelId
? state.models.baseModels.find(m => m.id === pipeline.sourceModelId)
: null;
// Pillar 2: Compute (0-25) — compute relative to parameter count (Chinchilla scaling)
const computePerParam = compute / Math.max(1, params);
const computeFactor = Math.min(25, Math.sqrt(computePerParam) * 8);
let rawCapability: number;
let capabilities: ModelCapabilities;
// Pillar 3: Data (0-20) — token count with quality multiplier
const dataQualityMultiplier = 1 + (researchBonuses?.dataQualityBonus ?? 0);
const dataFactor = Math.min(20, Math.log10(1 + dataTokens / 1e8) * 8 * dataQualityMultiplier);
if (sourceModel) {
rawCapability = Math.min(98, sourceModel.rawCapability * (1 + POINT_RELEASE_CAPABILITY_GAIN));
// Pillar 4: Research (0-20) — accumulated research knowledge
const capabilityResearchBonus = researchBonuses?.globalCapabilityBonus ?? 0;
const researchFactor = Math.min(20, capabilityResearchBonus + state.research.completedResearch.length * 0.5);
let rawCapability = Math.min(95, paramFactor + computeFactor + dataFactor + researchFactor);
if (architecture.type === 'moe') {
rawCapability = Math.min(98, rawCapability * MOE_CAPABILITY_MULTIPLIER);
}
// MoE tradeoff: total params need full VRAM even though only active params run
// This is enforced in the UI/store when checking VRAM requirements
const researcherQuality = state.talent.departments.research.effectiveness;
const contextBonus = Math.log2(Math.max(1, architecture.contextWindow / 4)) * 3;
const contextPenalty = Math.max(0, Math.log2(architecture.contextWindow / 8)) * 2;
const capabilities: ModelCapabilities = {
reasoning: clamp(rawCapability * (0.6 + dataMix.scientific * 0.5 + dataMix.code * 0.3) * (1 + researcherQuality * 0.2)),
coding: clamp(rawCapability * (0.5 + dataMix.code * 1.0)),
creative: clamp(rawCapability * (0.4 + dataMix.books * 0.6 + dataMix.conversation * 0.3)),
math: clamp(rawCapability * (0.3 + dataMix.scientific * 0.7 + dataMix.code * 0.2)),
knowledge: clamp(rawCapability * (0.5 + dataMix.web * 0.3 + dataMix.books * 0.3) + contextBonus * 0.3),
multimodal: clamp(rawCapability * (dataMix.images * 0.5 + dataMix.video * 0.4 + dataMix.audio * 0.2)),
agents: clamp(rawCapability * (0.2 + dataMix.code * 0.3 + dataMix.conversation * 0.2) + contextBonus * 0.5),
speed: Math.max(1, 100 - params * 0.3 - contextPenalty + (researchBonuses?.inferenceEfficiencyBonus ?? 0) * 20 + (architecture.type === 'moe' ? MOE_SPEED_MULTIPLIER * 10 : 0)),
contextUtilization: Math.min(100, architecture.contextWindow * 0.4),
};
if (researchBonuses) {
capabilities.reasoning = clamp(capabilities.reasoning + researchBonuses.reasoningBonus);
capabilities.coding = clamp(capabilities.coding + researchBonuses.codingBonus);
capabilities.creative = clamp(capabilities.creative + researchBonuses.creativeBonus);
capabilities.multimodal = clamp(capabilities.multimodal + researchBonuses.multimodalBonus);
capabilities.agents = clamp(capabilities.agents + researchBonuses.agentsBonus);
}
const breakthroughBonuses: Partial<Record<keyof ModelCapabilities, number>> = {};
for (const event of pipeline.events) {
if ((event.type === 'breakthrough' || event.type === 'emergent_capability') && event.impact.capabilityDomain && event.impact.capabilityBonus) {
const domain = event.impact.capabilityDomain;
breakthroughBonuses[domain] = (breakthroughBonuses[domain] ?? 0) + event.impact.capabilityBonus;
capabilities = { ...sourceModel.capabilities };
const boost = POINT_RELEASE_CAPABILITY_GAIN;
for (const key of Object.keys(capabilities) as (keyof ModelCapabilities)[]) {
if (key !== 'speed' && key !== 'contextUtilization') {
capabilities[key] = clamp(capabilities[key] * (1 + boost));
}
}
} else {
const paramFactor = Math.min(30, Math.log2(1 + params) * 4.5);
const computePerParam = compute / Math.max(1, params);
const computeFactor = Math.min(25, Math.sqrt(computePerParam) * 8);
const dataQualityMultiplier = 1 + (researchBonuses?.dataQualityBonus ?? 0);
const dataFactor = Math.min(20, Math.log10(1 + dataTokens / 1e8) * 8 * dataQualityMultiplier);
const capabilityResearchBonus = researchBonuses?.globalCapabilityBonus ?? 0;
const researchFactor = Math.min(20, capabilityResearchBonus + state.research.completedResearch.length * 0.5);
rawCapability = Math.min(95, paramFactor + computeFactor + dataFactor + researchFactor);
if (architecture.type === 'moe') {
rawCapability = Math.min(98, rawCapability * MOE_CAPABILITY_MULTIPLIER);
}
const researcherQuality = state.talent.departments.research.effectiveness;
const contextBonus = Math.log2(Math.max(1, architecture.contextWindow / 4)) * 3;
const contextPenalty = Math.max(0, Math.log2(architecture.contextWindow / 8)) * 2;
capabilities = {
reasoning: clamp(rawCapability * (0.6 + dataMix.scientific * 0.5 + dataMix.code * 0.3) * (1 + researcherQuality * 0.2)),
coding: clamp(rawCapability * (0.5 + dataMix.code * 1.0)),
creative: clamp(rawCapability * (0.4 + dataMix.books * 0.6 + dataMix.conversation * 0.3)),
math: clamp(rawCapability * (0.3 + dataMix.scientific * 0.7 + dataMix.code * 0.2)),
knowledge: clamp(rawCapability * (0.5 + dataMix.web * 0.3 + dataMix.books * 0.3) + contextBonus * 0.3),
multimodal: clamp(rawCapability * (dataMix.images * 0.5 + dataMix.video * 0.4 + dataMix.audio * 0.2)),
agents: clamp(rawCapability * (0.2 + dataMix.code * 0.3 + dataMix.conversation * 0.2) + contextBonus * 0.5),
speed: Math.max(1, 100 - params * 0.3 - contextPenalty + (researchBonuses?.inferenceEfficiencyBonus ?? 0) * 20 + (architecture.type === 'moe' ? MOE_SPEED_MULTIPLIER * 10 : 0)),
contextUtilization: Math.min(100, architecture.contextWindow * 0.4),
};
if (researchBonuses) {
capabilities.reasoning = clamp(capabilities.reasoning + researchBonuses.reasoningBonus);
capabilities.coding = clamp(capabilities.coding + researchBonuses.codingBonus);
capabilities.creative = clamp(capabilities.creative + researchBonuses.creativeBonus);
capabilities.multimodal = clamp(capabilities.multimodal + researchBonuses.multimodalBonus);
capabilities.agents = clamp(capabilities.agents + researchBonuses.agentsBonus);
}
const breakthroughBonuses: Partial<Record<keyof ModelCapabilities, number>> = {};
for (const event of pipeline.events) {
if ((event.type === 'breakthrough' || event.type === 'emergent_capability') && event.impact.capabilityDomain && event.impact.capabilityBonus) {
const domain = event.impact.capabilityDomain;
breakthroughBonuses[domain] = (breakthroughBonuses[domain] ?? 0) + event.impact.capabilityBonus;
}
}
for (const [domain, bonus] of Object.entries(breakthroughBonuses)) {
const key = domain as keyof ModelCapabilities;
capabilities[key] = clamp(capabilities[key] + bonus);
}
}
for (const [domain, bonus] of Object.entries(breakthroughBonuses)) {
const key = domain as keyof ModelCapabilities;
capabilities[key] = clamp(capabilities[key] + bonus);
}
const completedStages: ('pretraining' | 'sft' | 'alignment')[] = ['pretraining'];
if (pipeline.stages.sft?.isComplete) {
if (pipeline.stages.sft.isComplete) {
completedStages.push('sft');
const sft = pipeline.stages.sft;
for (let i = 0; i < sft.specializations.length; i++) {
const spec = sft.specializations[i];
const bonuses = SFT_SPECIALIZATION_BONUSES[spec];
if (!bonuses) continue;
const diminishing = i === 0 ? 1.0 : i === 1 ? 0.7 : 0.4;
for (const [cap, value] of Object.entries(bonuses)) {
const key = cap as keyof ModelCapabilities;
capabilities[key] = clamp(capabilities[key] + value * diminishing);
if (!sourceModel) {
for (let i = 0; i < sft.specializations.length; i++) {
const spec = sft.specializations[i];
const bonuses = SFT_SPECIALIZATION_BONUSES[spec];
if (!bonuses) continue;
const diminishing = i === 0 ? 1.0 : i === 1 ? 0.7 : 0.4;
for (const [cap, value] of Object.entries(bonuses)) {
const key = cap as keyof ModelCapabilities;
capabilities[key] = clamp(capabilities[key] + value * diminishing);
}
}
}
}
@@ -401,7 +378,7 @@ function createBaseModel(
let overallSafety = Math.min(100, 30 + safetyResearchBonus + Math.random() * 10);
let refusalRate = overallSafety > 60 ? 0.1 : 0.03;
if (pipeline.stages.alignment?.isComplete) {
if (pipeline.stages.alignment.isComplete) {
completedStages.push('alignment');
const alignment = pipeline.stages.alignment;
const methodConfig = ALIGNMENT_METHODS[alignment.method];
@@ -409,10 +386,12 @@ function createBaseModel(
const safetyGain = methodConfig.safetyGain * alignment.safetyWeight;
overallSafety = Math.min(100, overallSafety + safetyGain);
refusalRate = methodConfig.baseRefusal * Math.pow(alignment.safetyWeight, 1.5);
const capLoss = methodConfig.capabilityLoss * alignment.safetyWeight * 0.5;
for (const key of Object.keys(capabilities) as (keyof ModelCapabilities)[]) {
if (key !== 'speed' && key !== 'contextUtilization') {
capabilities[key] = clamp(capabilities[key] - capLoss);
if (!sourceModel) {
const capLoss = methodConfig.capabilityLoss * alignment.safetyWeight * 0.5;
for (const key of Object.keys(capabilities) as (keyof ModelCapabilities)[]) {
if (key !== 'speed' && key !== 'contextUtilization') {
capabilities[key] = clamp(capabilities[key] - capLoss);
}
}
}
}
@@ -426,10 +405,15 @@ function createBaseModel(
honesty: overallSafety * 0.9,
};
const family = state.models.families.find(f => f.id === pipeline.familyId);
const version = sourceModel ? Math.round((sourceModel.version + 0.1) * 10) / 10 : 1.0;
const familyName = family?.name ?? pipeline.modelName;
const autoName = `${familyName} ${SIZE_TIER_LABELS[pipeline.sizeTier]} v${version.toFixed(1)}`;
return {
id: uuid(),
familyId: pipeline.familyId,
name: pipeline.modelName,
name: autoName,
architecture,
dataMix,
capabilities,
@@ -439,6 +423,10 @@ function createBaseModel(
trainedAtTick: state.meta.tickCount,
trainingCostTotal: compute,
trainingStagesCompleted: completedStages,
sizeTier: pipeline.sizeTier,
version,
sftSpecializations: pipeline.stages.sft.specializations,
alignmentMethod: pipeline.stages.alignment.method,
};
}
@@ -467,59 +455,30 @@ function createVariant(job: VariantCreationJob, base: BaseModel): ModelVariant {
const caps = { ...base.capabilities };
let costMultiplier = 1.0;
let speedMultiplier = 1.0;
let variantName = base.name;
let arch = { ...base.architecture };
if (job.jobType === 'distillation' && 'targetParameters' in job.config) {
const config = job.config;
const sizeRatio = config.targetParameters / base.architecture.totalParameters;
const retention = DISTILLATION_BASE_RETENTION + sizeRatio * 0.25;
const config = job.config;
const qConfig = QUANTIZATION_CONFIGS[config.level];
if (qConfig) {
for (const key of Object.keys(caps) as (keyof ModelCapabilities)[]) {
caps[key] = clamp(caps[key] * retention);
if (key !== 'speed') caps[key] = clamp(caps[key] * qConfig.qualityRetention);
}
costMultiplier = sizeRatio * 0.8;
speedMultiplier = (1 / sizeRatio) * 0.7;
arch = { ...arch, totalParameters: config.targetParameters, activeParameters: config.targetParameters };
variantName = config.variantName;
} else if (job.jobType === 'fine-tuning' && 'specialization' in job.config) {
const config = job.config;
const bonuses = SFT_SPECIALIZATION_BONUSES[config.specialization];
if (bonuses) {
for (const [cap, value] of Object.entries(bonuses)) {
caps[cap as keyof ModelCapabilities] = clamp(caps[cap as keyof ModelCapabilities] + value);
}
}
variantName = config.variantName;
} else if (job.jobType === 'quantization' && 'level' in job.config) {
const config = job.config;
const qConfig = QUANTIZATION_CONFIGS[config.level];
if (qConfig) {
for (const key of Object.keys(caps) as (keyof ModelCapabilities)[]) {
if (key !== 'speed') caps[key] = clamp(caps[key] * qConfig.qualityRetention);
}
caps.speed = clamp(caps.speed * qConfig.speedMultiplier);
costMultiplier = qConfig.costMultiplier;
speedMultiplier = qConfig.speedMultiplier;
}
variantName = config.variantName;
caps.speed = clamp(caps.speed * qConfig.speedMultiplier);
costMultiplier = qConfig.costMultiplier;
speedMultiplier = qConfig.speedMultiplier;
}
return {
id: uuid(),
familyId: base.familyId,
baseModelId: base.id,
name: variantName,
variantType: job.jobType === 'distillation' ? 'distilled' : job.jobType === 'fine-tuning' ? 'fine-tuned' : 'quantized',
architecture: arch,
name: config.variantName,
variantType: 'quantized',
architecture: { ...base.architecture },
capabilities: caps,
safetyProfile: { ...base.safetyProfile },
isDeployed: false,
createdAtTick: 0,
quantization: job.jobType === 'quantization' && 'level' in job.config ? job.config.level : undefined,
distillationRetention: job.jobType === 'distillation' && 'targetParameters' in job.config
? DISTILLATION_BASE_RETENTION + (job.config.targetParameters / base.architecture.totalParameters) * 0.25
: undefined,
finetuneSpecialization: job.jobType === 'fine-tuning' && 'specialization' in job.config ? job.config.specialization : undefined,
quantization: config.level,
costMultiplier,
speedMultiplier,
};