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
+290 -299
View File
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { Play, Rocket, Globe, ChevronDown, ChevronUp, Beaker, Shield, Scissors, Wrench, Zap, BarChart3 } from 'lucide-react';
import { Play, Rocket, Globe, ChevronDown, ChevronUp, Beaker, Shield, Zap, BarChart3 } from 'lucide-react';
import { TutorialHint } from '@/components/game/TutorialHint';
import { ConfirmModal } from '@/components/common/ConfirmModal';
import { useGameStore } from '@/store';
@@ -9,10 +9,14 @@ import {
ALIGNMENT_METHODS,
QUANTIZATION_CONFIGS,
PARAMETER_OPTIONS,
SIZE_TIER_MAP,
SIZE_TIER_LABELS,
SFT_SPECIALIZATION_BONUSES,
} from '@ai-tycoon/shared';
import type {
ModelArchitecture, DataMixAllocation, SFTSpecialization, AlignmentMethod,
DataDomain, QuantizationLevel, BaseModel, ModelVariant, BenchmarkResult,
SizeTier, ModelFamily,
} from '@ai-tycoon/shared';
import { BENCHMARKS } from '@ai-tycoon/game-engine';
@@ -56,12 +60,8 @@ export function ModelsPage() {
const totalData = useGameStore((s) => s.data.totalTrainingTokens);
const currentEra = useGameStore((s) => s.meta.currentEra);
const startTrainingPipeline = useGameStore((s) => s.startTrainingPipeline);
const configureSFT = useGameStore((s) => s.configureSFT);
const configureAlignment = useGameStore((s) => s.configureAlignment);
const deployModel = useGameStore((s) => s.deployModel);
const deployVariant = useGameStore((s) => s.deployVariant);
const createDistillation = useGameStore((s) => s.createDistillation);
const createFineTune = useGameStore((s) => s.createFineTune);
const createQuantization = useGameStore((s) => s.createQuantization);
const startEvaluation = useGameStore((s) => s.startEvaluation);
const setTrainingAllocation = useGameStore((s) => s.setTrainingAllocation);
@@ -80,6 +80,15 @@ export function ModelsPage() {
const [dataMix, setDataMix] = useState<DataMixAllocation>({ ...DEFAULT_DATA_MIX });
const [dataMixPreset, setDataMixPreset] = useState('balanced');
// New model lifecycle state
const [familyMode, setFamilyMode] = useState<'new' | 'existing'>('new');
const [selectedFamilyId, setSelectedFamilyId] = useState<string | null>(null);
const [isPointRelease, setIsPointRelease] = useState(false);
const [sourceModelId, setSourceModelId] = useState<string | null>(null);
const [sftSpecs, setSftSpecs] = useState<SFTSpecialization[]>(['general']);
const [alignMethod, setAlignMethod] = useState<AlignmentMethod>('rlhf');
const [safetyWeight, setSafetyWeight] = useState(0.5);
const trainingFlops = totalFlops * trainingAlloc;
const estimatedTicks = trainingFlops > 0 ? Math.max(30, Math.ceil(180 / (1 + trainingFlops * 0.1))) : Infinity;
const estimatedCapability = Math.min(95, Math.sqrt(trainingFlops) * 5 + Math.log10(1 + totalData / 1e8) * 10);
@@ -95,9 +104,26 @@ export function ModelsPage() {
const currentEraIdx = eraOrder.indexOf(currentEra);
const availableBenchmarks = BENCHMARKS.filter(b => eraOrder.indexOf(b.unlockedAtEra) <= currentEraIdx);
const hasAlignmentResearch = completedResearch.some(r =>
r === 'alignment-research' || r === 'interpretability' || r === 'constitutional-ai',
);
// Computed size tier
const sizeTier: SizeTier = SIZE_TIER_MAP[parameterCount] ?? 'small';
// Model name preview
const familyNameForPreview = familyMode === 'new'
? (modelName.trim() || `Family ${families.length + 1}`)
: (families.find(f => f.id === selectedFamilyId)?.name ?? 'Family');
const nextVersion = (() => {
if (!isPointRelease || !sourceModelId) return 1.0;
const src = baseModels.find(m => m.id === sourceModelId);
return src ? Math.round((src.version + 0.1) * 10) / 10 : 1.0;
})();
const modelNamePreview = `${familyNameForPreview} ${SIZE_TIER_LABELS[sizeTier]} v${nextVersion.toFixed(1)}`;
const handleStartTraining = () => {
if (trainingFlops === 0) return;
const name = modelName.trim() || `Model v${families.length + 1}`;
const architecture: ModelArchitecture = {
type: archType,
@@ -109,14 +135,23 @@ export function ModelsPage() {
};
startTrainingPipeline({
modelName: name,
...(familyMode === 'new'
? { familyName: modelName.trim() || `Family ${families.length + 1}` }
: { familyId: selectedFamilyId! }),
architecture,
dataMix,
allocatedComputeFraction: 1.0,
targetTokens: totalData,
totalTicks: estimatedTicks,
sftSpecializations: sftSpecs,
alignmentMethod: alignMethod,
alignmentSafetyWeight: safetyWeight,
isPointRelease,
sourceModelId: sourceModelId ?? undefined,
});
setModelName('');
setIsPointRelease(false);
setSourceModelId(null);
};
const handlePresetChange = (presetKey: string) => {
@@ -137,10 +172,6 @@ export function ModelsPage() {
setDataMixPreset('custom');
};
const hasAlignmentResearch = completedResearch.some(r =>
r === 'alignment-research' || r === 'interpretability' || r === 'constitutional-ai',
);
return (
<div className="space-y-6">
<h2 className="text-2xl font-bold">Models</h2>
@@ -243,8 +274,8 @@ export function ModelsPage() {
<div className="flex gap-1 mb-2">
<StageBar label="Pre" active={pipeline.currentStage === 'pretraining'} complete={pipeline.stages.pretraining.isComplete} progress={pipeline.stages.pretraining.progressTicks / pipeline.stages.pretraining.totalTicks} />
<StageBar label="SFT" active={pipeline.currentStage === 'sft'} complete={pipeline.stages.sft?.isComplete ?? false} progress={pipeline.stages.sft ? pipeline.stages.sft.progressTicks / pipeline.stages.sft.totalTicks : 0} configured={!!pipeline.stages.sft} />
<StageBar label="Align" active={pipeline.currentStage === 'alignment'} complete={pipeline.stages.alignment?.isComplete ?? false} progress={pipeline.stages.alignment ? pipeline.stages.alignment.progressTicks / pipeline.stages.alignment.totalTicks : 0} configured={!!pipeline.stages.alignment} />
<StageBar label="SFT" active={pipeline.currentStage === 'sft'} complete={pipeline.stages.sft.isComplete} progress={pipeline.stages.sft.progressTicks / pipeline.stages.sft.totalTicks} />
<StageBar label="Align" active={pipeline.currentStage === 'alignment'} complete={pipeline.stages.alignment.isComplete} progress={pipeline.stages.alignment.progressTicks / pipeline.stages.alignment.totalTicks} />
</div>
<div className="h-2 bg-surface-800 rounded-full overflow-hidden">
@@ -259,19 +290,6 @@ export function ModelsPage() {
: `ETA: ${formatDuration(stage.totalTicks - stage.progressTicks)}`}
</div>
{pipeline.currentStage === 'pretraining' && !pipeline.stages.sft && !pipeline.stages.alignment && (
<div className="mt-2 flex items-center gap-2 text-xs text-warning">
<Beaker size={12} />
<span>
Post-training not configured {' '}
<button onClick={() => setExpandedPipeline(pipeline.id)} className="text-accent hover:text-accent-light underline">
configure SFT &amp; Alignment
</button>
{' '}or they'll be skipped.
</span>
</div>
)}
{isExpanded && (
<div className="mt-3 pt-3 border-t border-surface-700 space-y-2">
{pipeline.currentStage === 'pretraining' && (
@@ -281,10 +299,6 @@ export function ModelsPage() {
</div>
)}
{pipeline.currentStage === 'pretraining' && !pipeline.stages.pretraining.isComplete && (!pipeline.stages.sft || !pipeline.stages.alignment) && (
<PostTrainingConfig pipelineId={pipeline.id} hasAlignmentResearch={hasAlignmentResearch} completedResearch={completedResearch} configureSFT={configureSFT} configureAlignment={configureAlignment} sftConfigured={!!pipeline.stages.sft} alignmentConfigured={!!pipeline.stages.alignment} />
)}
{recentEvents.length > 0 && (
<div className="space-y-1">
<span className="text-xs text-surface-500 font-medium">Recent Events</span>
@@ -357,17 +371,60 @@ export function ModelsPage() {
{/* Train New Model */}
{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>
{isPointRelease && sourceModelId && (
<div className="bg-accent/10 border border-accent/30 rounded-lg px-3 py-2 flex items-center justify-between">
<div className="text-sm text-accent-light">
Point Release iterating on <span className="font-mono">{baseModels.find(m => m.id === sourceModelId)?.name ?? 'model'}</span>
<span className="text-xs text-surface-400 ml-2">(40% training time)</span>
</div>
<button onClick={() => { setIsPointRelease(false); setSourceModelId(null); }} className="text-xs text-surface-400 hover:text-surface-200">Cancel</button>
</div>
)}
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs text-surface-400 mb-1">Model Name</label>
{/* Family selector */}
<div>
<label className="block text-xs text-surface-400 mb-1">Model Family</label>
<div className="flex gap-2 mb-2">
<button
onClick={() => { setFamilyMode('new'); setIsPointRelease(false); setSourceModelId(null); }}
className={`flex-1 px-3 py-2 rounded text-sm border transition-colors ${familyMode === 'new' ? 'bg-accent/20 border-accent text-accent-light' : 'bg-surface-800 border-surface-600 text-surface-300'}`}
>
New Family
</button>
<button
onClick={() => setFamilyMode('existing')}
disabled={families.length === 0}
className={`flex-1 px-3 py-2 rounded text-sm border transition-colors ${familyMode === 'existing' ? 'bg-accent/20 border-accent text-accent-light' : 'bg-surface-800 border-surface-600 text-surface-300'} disabled:opacity-50 disabled:cursor-not-allowed`}
>
Add to Family
</button>
</div>
{familyMode === 'new' ? (
<input
type="text" value={modelName}
onChange={(e) => setModelName(e.target.value)}
placeholder={`Model v${families.length + 1}`}
placeholder={`Family ${families.length + 1}`}
className="w-full bg-surface-800 border border-surface-600 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50"
/>
</div>
) : (
<select
value={selectedFamilyId ?? ''}
onChange={(e) => setSelectedFamilyId(e.target.value || null)}
className="w-full bg-surface-800 border border-surface-600 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50"
>
<option value="">Select a family...</option>
{families.map(f => (
<option key={f.id} value={f.id}>{f.name} (Gen {f.generation})</option>
))}
</select>
)}
</div>
{/* Architecture & Parameters */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs text-surface-400 mb-1">Architecture</label>
<div className="flex gap-2">
@@ -385,20 +442,6 @@ export function ModelsPage() {
</button>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs text-surface-400 mb-1">Parameters (Billions)</label>
<select
value={parameterCount}
onChange={(e) => setParameterCount(Number(e.target.value))}
className="w-full bg-surface-800 border border-surface-600 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50"
>
{PARAMETER_OPTIONS.map(p => (
<option key={p} value={p}>{p}B</option>
))}
</select>
</div>
<div>
<label className="block text-xs text-surface-400 mb-1">Context Window</label>
<select
@@ -413,6 +456,25 @@ export function ModelsPage() {
</div>
</div>
{/* Parameters with size tier indicator */}
<div>
<label className="block text-xs text-surface-400 mb-1">Parameters (Billions)</label>
<div className="flex items-center gap-3">
<select
value={parameterCount}
onChange={(e) => setParameterCount(Number(e.target.value))}
className="flex-1 bg-surface-800 border border-surface-600 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50"
>
{PARAMETER_OPTIONS.map(p => (
<option key={p} value={p}>{p}B</option>
))}
</select>
<span className="text-xs px-2 py-1 rounded bg-accent/10 text-accent-light border border-accent/30">
{SIZE_TIER_LABELS[sizeTier]}
</span>
</div>
</div>
{/* Data Mix */}
<div>
<div className="flex items-center justify-between mb-2">
@@ -448,6 +510,94 @@ export function ModelsPage() {
</div>
</div>
{/* SFT Configuration */}
<div>
<div className="flex items-center gap-1 mb-2">
<Beaker size={12} className="text-surface-400" />
<label className="text-xs text-surface-400">SFT Specializations</label>
</div>
<div className="flex flex-wrap gap-1">
{SFT_OPTIONS.map(opt => (
<button
key={opt.value}
onClick={() => setSftSpecs(prev =>
prev.includes(opt.value)
? prev.filter(s => s !== opt.value)
: [...prev, opt.value]
)}
className={`px-2 py-1 rounded text-xs border transition-colors ${
sftSpecs.includes(opt.value)
? 'bg-accent/20 border-accent text-accent-light'
: 'bg-surface-800 border-surface-600 text-surface-400'
}`}
>
{opt.label}
</button>
))}
</div>
{sftSpecs.length > 0 && (
<div className="mt-2 text-[10px] text-surface-500">
<span className="font-medium text-surface-400">Bonus preview: </span>
{sftSpecs.map(spec => {
const bonuses = SFT_SPECIALIZATION_BONUSES[spec];
if (!bonuses) return null;
const positives = Object.entries(bonuses).filter(([, v]) => v > 0).map(([k, v]) => `${k} +${v}`);
const negatives = Object.entries(bonuses).filter(([, v]) => v < 0).map(([k, v]) => `${k} ${v}`);
return (
<span key={spec} className="inline-block mr-2">
<span className="text-accent-light">{spec}</span>
{positives.length > 0 && <span className="text-success ml-1">{positives.join(', ')}</span>}
{negatives.length > 0 && <span className="text-error ml-1">{negatives.join(', ')}</span>}
</span>
);
})}
</div>
)}
</div>
{/* Alignment Configuration */}
<div>
<div className="flex items-center gap-1 mb-2">
<Shield size={12} className="text-surface-400" />
<label className="text-xs text-surface-400">Alignment</label>
</div>
{hasAlignmentResearch ? (
<div className="space-y-2">
<div className="flex gap-1">
{(Object.keys(ALIGNMENT_METHODS) as AlignmentMethod[]).map(method => {
const isAvailable = completedResearch.includes(ALIGNMENT_METHODS[method].requiredResearch);
return (
<button
key={method}
disabled={!isAvailable}
onClick={() => setAlignMethod(method)}
className={`px-2 py-1 rounded text-xs border transition-colors ${
alignMethod === method ? 'bg-accent/20 border-accent text-accent-light' :
!isAvailable ? 'bg-surface-800 border-surface-700 text-surface-600 cursor-not-allowed' :
'bg-surface-800 border-surface-600 text-surface-400'
}`}
>
{method.toUpperCase()}
</button>
);
})}
</div>
<div className="flex items-center gap-2">
<span className="text-[10px] text-surface-400">Safety</span>
<input type="range" min={0} max={100} value={safetyWeight * 100}
onChange={(e) => setSafetyWeight(Number(e.target.value) / 100)}
className="flex-1 accent-accent h-1" />
<span className="text-[10px] text-surface-400">Helpful</span>
<span className="text-[10px] font-mono text-surface-500 w-8 text-right">{Math.round(safetyWeight * 100)}%</span>
</div>
</div>
) : (
<div className="text-xs text-surface-500 flex items-center gap-1">
<Shield size={10} /> Requires alignment research defaults to RLHF
</div>
)}
</div>
{/* Stats */}
<div className="grid grid-cols-4 gap-3 text-sm">
<div className="bg-surface-800 rounded-lg p-3">
@@ -471,14 +621,22 @@ export function ModelsPage() {
Estimated capability: <span className="text-accent-light font-mono">{estimatedCapability.toFixed(1)}/100</span>
{archType === 'moe' && <span className="ml-2 text-xs text-accent">(+15% MoE bonus)</span>}
</div>
{/* Model name preview */}
<div className="bg-surface-800/50 rounded-lg px-3 py-2 flex items-center gap-2">
<span className="text-[10px] text-surface-500">Model name:</span>
<span className="text-sm font-mono text-surface-300">{modelNamePreview}</span>
</div>
{/* Start button */}
<div>
<button
onClick={handleStartTraining}
disabled={trainingFlops === 0}
disabled={trainingFlops === 0 || (familyMode === 'existing' && !selectedFamilyId)}
className="flex items-center gap-2 bg-accent hover:bg-accent-dark text-white px-4 py-2 rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed"
>
<Play size={16} />
Start Pre-Training
{isPointRelease ? 'Start Point Release' : 'Start Training'}
</button>
{trainingFlops === 0 && totalFlops === 0 && (
<p className="text-xs text-warning mt-1">Build a data center and order racks first</p>
@@ -495,63 +653,77 @@ export function ModelsPage() {
<div className="space-y-3">
<h3 className="font-semibold">Model Families</h3>
{families.map(family => {
const base = baseModels.find(m => m.familyId === family.id);
const familyModels = baseModels.filter(m => m.familyId === family.id);
const variants = family.variants;
const isExpanded = expandedModel === family.id;
if (!base) return null;
return (
<div key={family.id} className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<div className="flex items-center justify-between">
{/* Family header */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<button
onClick={() => setExpandedModel(isExpanded ? null : family.id)}
className="text-surface-400 hover:text-surface-200"
>
<button onClick={() => setExpandedModel(isExpanded ? null : family.id)} className="text-surface-400 hover:text-surface-200">
{isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</button>
<div>
<h4 className="font-medium">{family.name} <span className="text-xs text-surface-500">Gen {family.generation}</span></h4>
<div className="text-xs text-surface-400">
{base.architecture.totalParameters}B {base.architecture.type.toUpperCase()} · Cap: {base.rawCapability.toFixed(1)} · Safety: {base.safetyProfile.overallSafety.toFixed(0)}
{variants.length > 0 && <span className="ml-1 text-surface-500">· {variants.length} variant{variants.length > 1 ? 's' : ''}</span>}
</div>
</div>
<h4 className="font-medium">{family.name} <span className="text-xs text-surface-500">Gen {family.generation}</span></h4>
</div>
<div className="flex items-center gap-2">
<ModelActions model={base} isOpenSourced={openSourcedModels.includes(base.id)} onDeploy={() => deployModel(base.id)} onOpenSource={() => openSourceModel(base.id)} />
<button onClick={() => { setModelsTab('train'); setFamilyMode('existing'); setSelectedFamilyId(family.id); }} className="text-xs text-accent hover:text-accent-light">
+ Train New Size
</button>
</div>
</div>
{isExpanded && (
{/* Model rows */}
{familyModels.map(model => (
<div key={model.id} className="flex items-center justify-between py-2 border-t border-surface-800 text-sm">
<div className="flex items-center gap-3">
<span className="font-medium">{model.name}</span>
<span className="text-xs text-surface-500">{model.architecture.totalParameters}B</span>
<span className="text-xs text-surface-500">Cap: {model.rawCapability.toFixed(1)}</span>
</div>
<div className="flex items-center gap-2">
{model.isDeployed ? (
<span className="text-xs px-2 py-0.5 rounded-full bg-success/20 text-success">Deployed</span>
) : (
<button onClick={() => deployModel(model.id)} className="text-xs bg-accent hover:bg-accent-dark text-white rounded px-2 py-1">Deploy</button>
)}
<button onClick={() => {
setModelsTab('train');
setFamilyMode('existing');
setSelectedFamilyId(family.id);
setIsPointRelease(true);
setSourceModelId(model.id);
setParameterCount(model.architecture.totalParameters);
setArchType(model.architecture.type);
setContextWindow(model.architecture.contextWindow);
setDataMix(model.dataMix);
setSftSpecs(model.sftSpecializations);
setAlignMethod(model.alignmentMethod ?? 'rlhf');
}} className="text-xs text-surface-400 hover:text-accent">Iterate</button>
</div>
</div>
))}
{familyModels.length === 0 && (
<p className="text-xs text-surface-500 py-2">Training in progress...</p>
)}
{/* Expanded: details, quantize, eval for each model */}
{isExpanded && familyModels.length > 0 && (
<div className="mt-4 pt-4 border-t border-surface-700 space-y-4">
{/* Base model details */}
<ModelDetails model={base} benchmarkResults={benchmarkResults} />
{familyModels.map(model => (
<div key={model.id} className="space-y-3">
<h5 className="text-sm font-medium text-surface-300">{model.name}</h5>
<ModelDetails model={model} benchmarkResults={benchmarkResults} />
<QuantizationCreator model={model} completedResearch={completedResearch} onQuantize={createQuantization} />
<BenchmarkEvaluator modelId={model.id} modelName={model.name} availableBenchmarks={availableBenchmarks} benchmarkResults={benchmarkResults} evalJobs={evalJobs} onStartEval={startEvaluation} />
</div>
))}
{/* Variant creation */}
<VariantCreator
model={base}
completedResearch={completedResearch}
onDistill={createDistillation}
onFineTune={createFineTune}
onQuantize={createQuantization}
/>
{/* Benchmark evaluation */}
<BenchmarkEvaluator
modelId={base.id}
modelName={base.name}
availableBenchmarks={availableBenchmarks}
benchmarkResults={benchmarkResults}
evalJobs={evalJobs}
onStartEval={startEvaluation}
/>
{/* Variants tree */}
{variants.length > 0 && (
<div className="space-y-2">
<span className="text-xs font-medium text-surface-300">Variants</span>
<span className="text-xs font-medium text-surface-300">Quantized Variants</span>
{variants.map(variant => (
<VariantCard
key={variant.id}
@@ -754,23 +926,16 @@ function ModelDetails({ model, benchmarkResults }: { model: BaseModel; benchmark
);
}
function VariantCreator({ model, completedResearch, onDistill, onFineTune, onQuantize }: {
function QuantizationCreator({ model, completedResearch, onQuantize }: {
model: BaseModel;
completedResearch: string[];
onDistill: (baseModelId: string, targetParams: number, name: string) => void;
onFineTune: (baseModelId: string, spec: SFTSpecialization, name: string) => void;
onQuantize: (baseModelId: string, level: QuantizationLevel, name: string) => void;
}) {
const [showCreator, setShowCreator] = useState(false);
const [creatorTab, setCreatorTab] = useState<'distill' | 'finetune' | 'quantize'>('quantize');
const [distillParams, setDistillParams] = useState(7);
const [ftSpec, setFtSpec] = useState<SFTSpecialization>('code');
const [quantLevel, setQuantLevel] = useState<QuantizationLevel>('int8');
const hasDistillation = completedResearch.includes('distillation');
const hasQuantization = completedResearch.includes('quantization') || completedResearch.includes('model-compression');
const smallerParams = PARAMETER_OPTIONS.filter(p => p < model.architecture.totalParameters);
if (!hasQuantization) return null;
if (!showCreator) {
return (
@@ -778,7 +943,7 @@ function VariantCreator({ model, completedResearch, onDistill, onFineTune, onQua
onClick={() => setShowCreator(true)}
className="flex items-center gap-1 text-xs text-accent hover:text-accent-light"
>
<Wrench size={12} /> Create Variant
<Zap size={12} /> Create Quantized Variant
</button>
);
}
@@ -786,91 +951,31 @@ function VariantCreator({ model, completedResearch, onDistill, onFineTune, onQua
return (
<div className="bg-surface-800/50 rounded-lg p-3 space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-surface-300">Create Variant</span>
<span className="text-xs font-medium text-surface-300">Quantize {model.name}</span>
<button onClick={() => setShowCreator(false)} className="text-xs text-surface-500 hover:text-surface-300">Close</button>
</div>
<div className="flex gap-1">
{hasDistillation && (
<button onClick={() => setCreatorTab('distill')}
className={`flex items-center gap-1 px-2 py-1 rounded text-[10px] border ${creatorTab === 'distill' ? 'bg-accent/20 border-accent text-accent-light' : 'bg-surface-800 border-surface-600 text-surface-400'}`}>
<Scissors size={10} /> Distill
</button>
)}
<button onClick={() => setCreatorTab('finetune')}
className={`flex items-center gap-1 px-2 py-1 rounded text-[10px] border ${creatorTab === 'finetune' ? 'bg-accent/20 border-accent text-accent-light' : 'bg-surface-800 border-surface-600 text-surface-400'}`}>
<Wrench size={10} /> Fine-tune
</button>
{hasQuantization && (
<button onClick={() => setCreatorTab('quantize')}
className={`flex items-center gap-1 px-2 py-1 rounded text-[10px] border ${creatorTab === 'quantize' ? 'bg-accent/20 border-accent text-accent-light' : 'bg-surface-800 border-surface-600 text-surface-400'}`}>
<Zap size={10} /> Quantize
</button>
)}
</div>
{creatorTab === 'distill' && hasDistillation && (
<div className="space-y-2">
<div>
<label className="text-[10px] text-surface-400">Target Size</label>
<select value={distillParams} onChange={e => setDistillParams(Number(e.target.value))}
className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-xs mt-0.5">
{smallerParams.map(p => <option key={p} value={p}>{p}B</option>)}
</select>
</div>
<div className="text-[10px] text-surface-500">
Retention: ~{((0.70 + (distillParams / model.architecture.totalParameters) * 0.25) * 100).toFixed(0)}% quality
</div>
<button onClick={() => { onDistill(model.id, distillParams, `${model.name}-${distillParams}B`); setShowCreator(false); }}
className="bg-accent hover:bg-accent-dark text-white rounded px-3 py-1 text-xs">
Start Distillation
</button>
</div>
)}
{creatorTab === 'finetune' && (
<div className="space-y-2">
<div>
<label className="text-[10px] text-surface-400">Specialization</label>
<div className="flex flex-wrap gap-1 mt-0.5">
{SFT_OPTIONS.map(opt => (
<button key={opt.value} onClick={() => setFtSpec(opt.value)}
className={`px-2 py-0.5 rounded text-[10px] border ${ftSpec === opt.value ? 'bg-accent/20 border-accent text-accent-light' : 'bg-surface-800 border-surface-600 text-surface-400'}`}>
{opt.label}
<div className="space-y-2">
<div>
<label className="text-[10px] text-surface-400">Quantization Level</label>
<div className="flex gap-1 mt-0.5">
{(Object.keys(QUANTIZATION_CONFIGS) as QuantizationLevel[]).map(level => {
const cfg = QUANTIZATION_CONFIGS[level];
return (
<button key={level} onClick={() => setQuantLevel(level)}
className={`flex-1 px-2 py-1 rounded text-[10px] border ${quantLevel === level ? 'bg-accent/20 border-accent text-accent-light' : 'bg-surface-800 border-surface-600 text-surface-400'}`}>
<div>{QUANT_LABELS[level]}</div>
<div className="text-[9px] text-surface-500">{(cfg.qualityRetention * 100).toFixed(0)}% · {cfg.speedMultiplier}x</div>
</button>
))}
</div>
);
})}
</div>
<button onClick={() => { onFineTune(model.id, ftSpec, `${model.name}-${ftSpec.charAt(0).toUpperCase() + ftSpec.slice(1)}`); setShowCreator(false); }}
className="bg-accent hover:bg-accent-dark text-white rounded px-3 py-1 text-xs">
Start Fine-Tuning
</button>
</div>
)}
{creatorTab === 'quantize' && hasQuantization && (
<div className="space-y-2">
<div>
<label className="text-[10px] text-surface-400">Quantization Level</label>
<div className="flex gap-1 mt-0.5">
{(Object.keys(QUANTIZATION_CONFIGS) as QuantizationLevel[]).map(level => {
const cfg = QUANTIZATION_CONFIGS[level];
return (
<button key={level} onClick={() => setQuantLevel(level)}
className={`flex-1 px-2 py-1 rounded text-[10px] border ${quantLevel === level ? 'bg-accent/20 border-accent text-accent-light' : 'bg-surface-800 border-surface-600 text-surface-400'}`}>
<div>{QUANT_LABELS[level]}</div>
<div className="text-[9px] text-surface-500">{(cfg.qualityRetention * 100).toFixed(0)}% · {cfg.speedMultiplier}x</div>
</button>
);
})}
</div>
</div>
<button onClick={() => { onQuantize(model.id, quantLevel, `${model.name}-Turbo`); setShowCreator(false); }}
className="bg-accent hover:bg-accent-dark text-white rounded px-3 py-1 text-xs">
Start Quantization
</button>
</div>
)}
<button onClick={() => { onQuantize(model.id, quantLevel, `${model.name}-Turbo`); setShowCreator(false); }}
className="bg-accent hover:bg-accent-dark text-white rounded px-3 py-1 text-xs">
Start Quantization
</button>
</div>
</div>
);
}
@@ -961,11 +1066,6 @@ function VariantCard({ variant, familyId, benchmarkResults, availableBenchmarks,
const [isExpanded, setIsExpanded] = useState(false);
const variantResults = benchmarkResults.filter(r => r.modelId === variant.id);
const typeLabel = variant.variantType === 'distilled' ? 'Distilled'
: variant.variantType === 'fine-tuned' ? 'Fine-tuned' : 'Quantized';
const typeColor = variant.variantType === 'distilled' ? 'text-purple-400'
: variant.variantType === 'fine-tuned' ? 'text-yellow-400' : 'text-green-400';
return (
<div className="bg-surface-800/50 rounded-lg p-3 ml-4 border-l-2 border-surface-600">
<div className="flex items-center justify-between">
@@ -975,9 +1075,8 @@ function VariantCard({ variant, familyId, benchmarkResults, availableBenchmarks,
</button>
<div>
<span className="text-sm font-medium">{variant.name}</span>
<span className={`text-[10px] ml-2 ${typeColor}`}>{typeLabel}</span>
<span className="text-[10px] ml-2 text-green-400">Quantized</span>
{variant.quantization && <span className="text-[10px] text-surface-500 ml-1">{variant.quantization.toUpperCase()}</span>}
{variant.finetuneSpecialization && <span className="text-[10px] text-surface-500 ml-1">{variant.finetuneSpecialization}</span>}
</div>
</div>
<div className="flex items-center gap-2">
@@ -1108,16 +1207,15 @@ function BenchmarkLeaderboard({ benchmarkResults, baseModels, families, availabl
);
}
function StageBar({ label, active, complete, progress, configured = true }: {
label: string; active: boolean; complete: boolean; progress: number; configured?: boolean;
function StageBar({ label, active, complete, progress }: {
label: string; active: boolean; complete: boolean; progress: number;
}) {
return (
<div className="flex-1">
<div className={`text-[9px] text-center mb-0.5 ${!configured ? 'text-warning' : 'text-surface-500'}`}>
{label}{!configured && ' (skip)'}
<div className="text-[9px] text-center mb-0.5 text-surface-500">
{label}
</div>
<div className={`h-1 rounded-full ${
!configured ? 'bg-surface-800 border border-dashed border-warning/30' :
complete ? 'bg-success' :
active ? 'bg-accent' :
'bg-surface-700'
@@ -1129,110 +1227,3 @@ function StageBar({ label, active, complete, progress, configured = true }: {
</div>
);
}
function PostTrainingConfig({ pipelineId, hasAlignmentResearch, completedResearch, configureSFT, configureAlignment, sftConfigured, alignmentConfigured }: {
pipelineId: string;
hasAlignmentResearch: boolean;
completedResearch: string[];
configureSFT: (pipelineId: string, specializations: SFTSpecialization[]) => void;
configureAlignment: (pipelineId: string, method: AlignmentMethod, safetyWeight: number) => void;
sftConfigured: boolean;
alignmentConfigured: boolean;
}) {
const [selectedSpecs, setSelectedSpecs] = useState<SFTSpecialization[]>(['general']);
const [alignMethod, setAlignMethod] = useState<AlignmentMethod>('rlhf');
const [safetyWeight, setSafetyWeight] = useState(0.5);
return (
<div className="space-y-3 bg-surface-800/50 rounded-lg p-3">
<div className="text-xs font-medium text-surface-300">Configure Post-Training (optional)</div>
{!sftConfigured ? (
<div className="space-y-1">
<div className="flex items-center gap-1 text-xs text-surface-400">
<Beaker size={10} /> Supervised Fine-Tuning
</div>
<div className="flex flex-wrap gap-1">
{SFT_OPTIONS.map(opt => (
<button
key={opt.value}
onClick={() => setSelectedSpecs(prev =>
prev.includes(opt.value)
? prev.filter(s => s !== opt.value)
: [...prev, opt.value]
)}
className={`px-2 py-0.5 rounded text-[10px] border transition-colors ${
selectedSpecs.includes(opt.value)
? 'bg-accent/20 border-accent text-accent-light'
: 'bg-surface-800 border-surface-600 text-surface-400'
}`}
>
{opt.label}
</button>
))}
</div>
<button
onClick={() => configureSFT(pipelineId, selectedSpecs)}
className="text-[10px] text-accent hover:text-accent-light"
>
Enable SFT
</button>
</div>
) : (
<div className="flex items-center gap-1 text-xs text-success">
<Beaker size={10} /> SFT configured
</div>
)}
{!alignmentConfigured ? (
hasAlignmentResearch ? (
<div className="space-y-1">
<div className="flex items-center gap-1 text-xs text-surface-400">
<Shield size={10} /> Alignment
</div>
<div className="flex gap-1">
{(Object.keys(ALIGNMENT_METHODS) as AlignmentMethod[]).map(method => {
const isAvailable = completedResearch.includes(ALIGNMENT_METHODS[method].requiredResearch);
return (
<button
key={method}
disabled={!isAvailable}
onClick={() => setAlignMethod(method)}
className={`px-2 py-0.5 rounded text-[10px] border transition-colors ${
alignMethod === method ? 'bg-accent/20 border-accent text-accent-light' :
!isAvailable ? 'bg-surface-800 border-surface-700 text-surface-600 cursor-not-allowed' :
'bg-surface-800 border-surface-600 text-surface-400'
}`}
>
{method.toUpperCase()}
</button>
);
})}
</div>
<div className="flex items-center gap-2">
<span className="text-[10px] text-surface-400">Safety</span>
<input type="range" min={0} max={100} value={safetyWeight * 100}
onChange={(e) => setSafetyWeight(Number(e.target.value) / 100)}
className="flex-1 accent-accent h-1" />
<span className="text-[10px] text-surface-400">Helpful</span>
</div>
<button
onClick={() => configureAlignment(pipelineId, alignMethod, safetyWeight)}
className="text-[10px] text-accent hover:text-accent-light"
>
Enable Alignment
</button>
</div>
) : (
<div className="flex items-center gap-1 text-xs text-surface-500">
<Shield size={10} /> Alignment requires research
</div>
)
) : (
<div className="flex items-center gap-1 text-xs text-success">
<Shield size={10} /> Alignment configured
</div>
)}
</div>
);
}
+94 -120
View File
@@ -13,7 +13,7 @@ import type {
CoolingType, NetworkFabric,
FundingRoundType, OverloadPolicy,
TrainingPipeline, ModelFamily, DataMixAllocation,
ModelArchitecture,
ModelArchitecture, AlignmentMethod, SizeTier,
SFTSpecialization, QuantizationLevel, VariantCreationJob,
EvalJob,
ConsumerTierId, ApiTierId,
@@ -36,9 +36,10 @@ import {
COOLING_TYPE_CONFIGS, COOLING_ORDER, NETWORK_FABRIC_CONFIGS, FABRIC_ORDER,
DEFAULT_DATA_MIX,
MAX_CONCURRENT_TRAINING,
DISTILLATION_TIME_FRACTION, DISTILLATION_COMPUTE_FRACTION,
FINETUNE_TIME_FRACTION, FINETUNE_COMPUTE_FRACTION,
QUANTIZATION_TICKS,
SFT_TIME_FRACTION, ALIGNMENT_TIME_FRACTION,
SIZE_TIER_MAP, SIZE_TIER_LABELS,
POINT_RELEASE_TIME_FRACTION, POINT_RELEASE_MAX_VERSION,
} from '@ai-tycoon/shared';
import {
emptyDCNetworkSummary, emptyCampusNetworkSummary, emptyClusterNetworkSummary,
@@ -115,11 +116,21 @@ interface Actions {
upgradeDataCenter: (dataCenterId: string, upgrade: 'cooling' | 'redundancy') => void;
upgradeCoolingType: (dataCenterId: string, targetCooling: CoolingType) => void;
upgradeNetworkFabric: (dataCenterId: string, targetFabric: NetworkFabric) => void;
startTrainingPipeline: (config: { modelName: string; architecture: ModelArchitecture; dataMix: DataMixAllocation; allocatedComputeFraction: number; targetTokens: number; totalTicks: number }) => void;
configureSFT: (pipelineId: string, specializations: import('@ai-tycoon/shared').SFTSpecialization[]) => void;
configureAlignment: (pipelineId: string, method: import('@ai-tycoon/shared').AlignmentMethod, safetyWeight: number) => void;
createDistillation: (baseModelId: string, targetParameters: number, variantName: string) => void;
createFineTune: (baseModelId: string, specialization: SFTSpecialization, variantName: string) => void;
startTrainingPipeline: (config: {
familyId?: string;
familyName?: string;
architecture: ModelArchitecture;
dataMix: DataMixAllocation;
allocatedComputeFraction: number;
targetTokens: number;
totalTicks: number;
sftSpecializations: SFTSpecialization[];
alignmentMethod: AlignmentMethod;
alignmentSafetyWeight: number;
isPointRelease?: boolean;
sourceModelId?: string;
}) => void;
startPointRelease: (baseModelId: string) => void;
createQuantization: (baseModelId: string, level: QuantizationLevel, variantName: string) => void;
startEvaluation: (modelId: string, benchmarkIds: string[]) => void;
deployModel: (modelId: string) => void;
@@ -917,29 +928,52 @@ export const useGameStore = create<Store>()(
startTrainingPipeline: (config) => {
let created = false;
let toastName = '';
set((s) => {
const activeCount = s.models.activeTrainingPipelines.filter(p => p.status === 'active' || p.status === 'stalled').length;
const maxSlots = MAX_CONCURRENT_TRAINING[s.meta.currentEra] ?? 1;
if (activeCount >= maxSlots) return s;
created = true;
const familyId = uuid();
const pipelineId = uuid();
const generation = s.models.families.length + 1;
const family: ModelFamily = {
id: familyId,
name: config.modelName,
generation,
baseModelId: null,
variants: [],
createdAtTick: s.meta.tickCount,
};
let familyId: string;
let updatedFamilies = [...s.models.families];
if (config.familyId) {
familyId = config.familyId;
} else {
familyId = uuid();
const generation = s.models.families.length + 1;
const family: ModelFamily = {
id: familyId,
name: config.familyName ?? 'Model',
generation,
baseModelIds: [],
variants: [],
createdAtTick: s.meta.tickCount,
};
updatedFamilies = [...updatedFamilies, family];
}
const sizeTier: SizeTier = SIZE_TIER_MAP[config.architecture.totalParameters] ?? 'small';
const familyName = config.familyName ?? updatedFamilies.find(f => f.id === familyId)?.name ?? 'Model';
const version = config.isPointRelease && config.sourceModelId
? (() => {
const src = s.models.baseModels.find(m => m.id === config.sourceModelId);
return src ? Math.round((src.version + 0.1) * 10) / 10 : 1.0;
})()
: 1.0;
const modelName = `${familyName} ${SIZE_TIER_LABELS[sizeTier]} v${version.toFixed(1)}`;
toastName = modelName;
const baseTotalTicks = config.isPointRelease
? Math.ceil(config.totalTicks * POINT_RELEASE_TIME_FRACTION)
: config.totalTicks;
const pipeline: TrainingPipeline = {
id: pipelineId,
id: uuid(),
familyId,
modelName: config.modelName,
modelName,
architecture: config.architecture,
dataMix: config.dataMix,
currentStage: 'pretraining',
@@ -949,130 +983,70 @@ export const useGameStore = create<Store>()(
processedTokens: 0,
computeAllocated: 0,
progressTicks: 0,
totalTicks: config.totalTicks,
totalTicks: baseTotalTicks,
lossValue: 10,
chinchillaRatio: config.targetTokens / (config.architecture.totalParameters * 1e9),
isComplete: false,
},
sft: null,
alignment: null,
sft: {
specializations: config.sftSpecializations,
progressTicks: 0,
totalTicks: Math.ceil(baseTotalTicks * SFT_TIME_FRACTION),
isComplete: false,
},
alignment: {
method: config.alignmentMethod,
safetyWeight: config.alignmentSafetyWeight,
helpfulnessWeight: 1 - config.alignmentSafetyWeight,
progressTicks: 0,
totalTicks: Math.ceil(baseTotalTicks * ALIGNMENT_TIME_FRACTION),
isComplete: false,
},
},
status: 'active',
allocatedComputeFraction: config.allocatedComputeFraction,
events: [],
startedAtTick: s.meta.tickCount,
sizeTier,
isPointRelease: config.isPointRelease ?? false,
sourceModelId: config.sourceModelId ?? null,
};
return {
models: {
...s.models,
families: [...s.models.families, family],
families: updatedFamilies,
activeTrainingPipelines: [...s.models.activeTrainingPipelines, pipeline],
},
};
});
if (created) {
get().addNotification({ title: 'Training Started', message: `${config.modelName} pre-training has begun.`, type: 'info', tick: get().meta.tickCount });
get().addNotification({ title: 'Training Started', message: `${toastName} training has begun.`, type: 'info', tick: get().meta.tickCount });
set({ modelsTab: 'overview' as ModelsTab });
}
},
configureSFT: (pipelineId, specializations) => {
set((s) => ({
models: {
...s.models,
activeTrainingPipelines: s.models.activeTrainingPipelines.map(p =>
p.id === pipelineId ? {
...p,
stages: {
...p.stages,
sft: {
specializations,
progressTicks: 0,
totalTicks: Math.ceil(p.stages.pretraining.totalTicks * 0.10),
isComplete: false,
},
},
} : p,
),
},
}));
get().addNotification({ title: 'SFT Configured', message: `${specializations.join(', ')} specializations enabled.`, type: 'success', tick: get().meta.tickCount });
},
startPointRelease: (baseModelId) => {
const s = get();
const base = s.models.baseModels.find(m => m.id === baseModelId);
if (!base) return;
if (base.version >= POINT_RELEASE_MAX_VERSION) return;
const family = s.models.families.find(f => f.id === base.familyId);
if (!family) return;
configureAlignment: (pipelineId, method, safetyWeight) => {
set((s) => ({
models: {
...s.models,
activeTrainingPipelines: s.models.activeTrainingPipelines.map(p =>
p.id === pipelineId ? {
...p,
stages: {
...p.stages,
alignment: {
method,
safetyWeight,
helpfulnessWeight: 1 - safetyWeight,
progressTicks: 0,
totalTicks: Math.ceil(p.stages.pretraining.totalTicks * 0.08),
isComplete: false,
},
},
} : p,
),
},
}));
get().addNotification({ title: 'Alignment Configured', message: `${method.toUpperCase()} alignment enabled.`, type: 'success', tick: get().meta.tickCount });
},
createDistillation: (baseModelId, targetParameters, variantName) => {
let created = false;
set((s) => {
const base = s.models.baseModels.find(m => m.id === baseModelId);
if (!base) return s;
created = true;
const job: VariantCreationJob = {
id: uuid(),
familyId: base.familyId,
baseModelId,
jobType: 'distillation',
config: { targetParameters, targetArchitecture: base.architecture.type, variantName },
progressTicks: 0,
totalTicks: Math.ceil(base.trainingCostTotal > 0 ? DISTILLATION_TIME_FRACTION * 120 : 30),
allocatedComputeFraction: DISTILLATION_COMPUTE_FRACTION,
status: 'active',
};
return { models: { ...s.models, variantJobs: [...s.models.variantJobs, job] } };
get().startTrainingPipeline({
familyId: base.familyId,
architecture: base.architecture,
dataMix: base.dataMix,
allocatedComputeFraction: 1.0,
targetTokens: base.architecture.totalParameters * 20e9,
totalTicks: Math.ceil(base.architecture.totalParameters * 2 + 60),
sftSpecializations: base.sftSpecializations,
alignmentMethod: base.alignmentMethod ?? 'rlhf',
alignmentSafetyWeight: 0.5,
isPointRelease: true,
sourceModelId: baseModelId,
});
if (created) {
get().addNotification({ title: 'Distillation Started', message: `${variantName} distillation in progress.`, type: 'info', tick: get().meta.tickCount });
set({ modelsTab: 'overview' as ModelsTab });
}
},
createFineTune: (baseModelId, specialization, variantName) => {
let created = false;
set((s) => {
const base = s.models.baseModels.find(m => m.id === baseModelId);
if (!base) return s;
created = true;
const job: VariantCreationJob = {
id: uuid(),
familyId: base.familyId,
baseModelId,
jobType: 'fine-tuning',
config: { specialization, datasetIds: [], variantName },
progressTicks: 0,
totalTicks: Math.ceil(FINETUNE_TIME_FRACTION * 120),
allocatedComputeFraction: FINETUNE_COMPUTE_FRACTION,
status: 'active',
};
return { models: { ...s.models, variantJobs: [...s.models.variantJobs, job] } };
});
if (created) {
get().addNotification({ title: 'Fine-Tuning Started', message: `${variantName} fine-tuning in progress.`, type: 'info', tick: get().meta.tickCount });
set({ modelsTab: 'overview' as ModelsTab });
}
},
createQuantization: (baseModelId, level, variantName) => {