1- /**
2- * CLI entry point - runs the simulation engine from Node.js.
3- *
4- * Usage:
5- * npx tsx src/cli/index.ts
6- * npx tsx src/cli/index.ts --limit 2
7- *
8- * Shares the exact same simulation engine (simulation.ts) with the web app.
9- * Only the data-loading layer differs (fs instead of fetch).
10- */
11-
121import { readFileSync , writeFileSync } from "node:fs" ;
132import { resolve , dirname } from "node:path" ;
143import { fileURLToPath } from "node:url" ;
15- import type { Constants , TrainingProfile , Scenario , CO2Timeline , FullProfile } from "../types" ;
16- import { simulateStepwise , buildResult , tokensPerSecond } from "../data/simulation.js" ;
4+ import type { Constants , TrainingProfile , Scenario , CO2Timeline , FullProfile , SimConfig } from "../domain/types" ;
5+ import { simulateStepwise } from "../domain/simulation" ;
6+ import { neverPausePolicy , hysteresisPolicy } from "../domain/policy" ;
7+ import { buildSimResult } from "../domain/result" ;
8+ import { tokensPerSecond } from "../domain/physics" ;
179
1810const __dirname = dirname ( fileURLToPath ( import . meta. url ) ) ;
1911const DATA_DIR = resolve ( __dirname , "../../public/data" ) ;
@@ -75,6 +67,8 @@ interface CliResult {
7567 issues : string ;
7668}
7769
70+ function r6 ( n : number ) { return Math . round ( n * 1_000_000 ) / 1_000_000 ; }
71+
7872async function main ( ) {
7973 const args = process . argv . slice ( 2 ) ;
8074 const limitIdx = args . indexOf ( "--limit" ) ;
@@ -102,72 +96,71 @@ async function main() {
10296
10397 for ( const startTime of sc . startTimes ) {
10498 for ( let ti = 0 ; ti < sc . thresholds . length ; ti ++ ) {
105- const config = {
106- scenarioDescription : sc . description ,
107- region : sc . region ,
108- historicalYears : sc . historicalYears ,
99+ const simConfig : SimConfig = {
109100 startTime,
110- thetaPause : sc . thresholds [ ti ] ,
111- thetaResume : sc . hysteresis [ ti ] ,
101+ historicalYears : sc . historicalYears ,
112102 overheadBudgetPct : sc . overheadBudgetPct ,
113103 } ;
104+ const thetaPause = sc . thresholds [ ti ] ;
105+ const thetaResume = sc . hysteresis [ ti ] ;
106+ const policy = hysteresisPolicy ( thetaPause , thetaResume ) ;
107+ const baselinePolicy = neverPausePolicy ( ) ;
114108
115- const baselineConfig = { ...config , thetaPause : Infinity , thetaResume : 0 } ;
116-
117- const progress : Parameters < typeof buildResult > [ 2 ] [ ] = [ ] ;
118- for ( const p of simulateStepwise ( full , config , tl ) ) progress . push ( p ) ;
119- const last = progress [ progress . length - 1 ] ;
109+ const progress : Parameters < typeof simulateStepwise > [ 3 ] [ ] = [ ] ;
110+ for ( const p of simulateStepwise ( full , baselinePolicy , tl , simConfig ) ) progress . push ( p ) ;
111+ const lastBaseline = progress [ progress . length - 1 ] ;
120112
121- const baselineProgress : Parameters < typeof buildResult > [ 2 ] [ ] = [ ] ;
122- for ( const p of simulateStepwise ( full , baselineConfig , tl ) ) baselineProgress . push ( p ) ;
123- const lastBaseline = baselineProgress [ baselineProgress . length - 1 ] ;
113+ const simProgress : Parameters < typeof simulateStepwise > [ 3 ] [ ] = [ ] ;
114+ for ( const p of simulateStepwise ( full , policy , tl , simConfig ) ) simProgress . push ( p ) ;
115+ const last = simProgress [ simProgress . length - 1 ] ;
124116
125- const meta = buildResult ( full , config , last , lastBaseline ) ;
126-
127- const idealS = last . tokensTotal / ( tokensPerSecond ( profile . gpuCount ) || 1 ) ;
128- const actualOverheadPct = 100 * ( last . pausedS + last . checkpointS ) / ( idealS || 1 ) ;
129- const co2SavingsPct = meta . baselineEmissionsKgco2 > 0
130- ? ( meta . baselineEmissionsKgco2 - meta . totalEmissionsKgco2 ) / meta . baselineEmissionsKgco2 * 100
131- : 0 ;
132- const score = co2SavingsPct / Math . max ( actualOverheadPct , 0.001 ) ;
117+ const result = buildSimResult ( full , simConfig , last , lastBaseline , thetaPause , thetaResume , {
118+ id : `cli-${ Date . now ( ) } ` ,
119+ scenarioDescription : sc . description ,
120+ model : profile . name ,
121+ region : sc . region ,
122+ timestamps : [ ] ,
123+ carbonIntensitySeries : [ ] ,
124+ stateSeries : [ ] ,
125+ emissionsSeries : [ ] ,
126+ tokensRemainingSeries : [ ] ,
127+ } ) ;
133128
134- const isOk = last . done && meta . withinOverheadBudget && last . issues . length === 0 ;
135- if ( isOk ) ok ++ ; else fail ++ ;
129+ if ( result . ok ) ok ++ ; else fail ++ ;
136130
137- function r6 ( n : number ) { return Math . round ( n * 1_000_000 ) / 1_000_000 ; }
138131 results . push ( {
139132 scenario : sc . description ,
140133 model : profile . name ,
141134 region : sc . region ,
142135 historicalYears : sc . historicalYears . join ( ";" ) ,
143136 startTime,
144- thetaPause : config . thetaPause ,
145- thetaResume : config . thetaResume ,
146- overheadBudgetPct : config . overheadBudgetPct ,
147- totalWallTimeH : r6 ( last . totalWallS / 3600 ) ,
148- trainingTimeH : r6 ( last . trainingS / 3600 ) ,
149- pausedTimeH : r6 ( last . pausedS / 3600 ) ,
150- checkpointOverheadH : r6 ( last . checkpointS / 3600 ) ,
151- totalEnergyKwh : r6 ( last . totalEnergyWh / 1000 ) ,
152- trainingEnergyKwh : r6 ( last . trainingEnergyWh / 1000 ) ,
153- pausedEnergyKwh : r6 ( last . pausedEnergyWh / 1000 ) ,
154- checkpointEnergyKwh : r6 ( last . checkpointEnergyWh / 1000 ) ,
155- totalEmissionsKgco2 : r6 ( last . totalEmissionsG / 1000 ) ,
156- tokensProcessed : last . tokensTotal - last . tokensRemaining ,
157- tokensTotal : last . tokensTotal ,
158- completed : last . done && last . tokensRemaining <= 0 ,
159- numPauses : last . numPauses ,
160- actualOverheadPct : r6 ( actualOverheadPct ) ,
161- withinOverheadBudget : meta . withinOverheadBudget ,
162- baselineEmissionsKgco2 : r6 ( meta . baselineEmissionsKgco2 ) ,
163- baselineTimeH : r6 ( meta . baselineTimeH ) ,
164- co2SavingsPct : r6 ( co2SavingsPct ) ,
165- score : r6 ( score ) ,
166- idleTimeH : r6 ( ( last . pausedS + last . checkpointS ) / 3600 ) ,
167- completionPct : last . tokensTotal > 0 ? r6 ( 100 * ( last . tokensTotal - last . tokensRemaining ) / last . tokensTotal ) : 0 ,
168- ok : isOk ,
169- stopReason : last . stopReason ,
170- issues : last . issues . join ( "; " ) ,
137+ thetaPause,
138+ thetaResume,
139+ overheadBudgetPct : sc . overheadBudgetPct ,
140+ totalWallTimeH : r6 ( result . totalWallTimeH ) ,
141+ trainingTimeH : r6 ( result . trainingTimeH ) ,
142+ pausedTimeH : r6 ( result . pausedTimeH ) ,
143+ checkpointOverheadH : r6 ( result . checkpointOverheadH ) ,
144+ totalEnergyKwh : r6 ( result . totalEnergyKwh ) ,
145+ trainingEnergyKwh : r6 ( result . trainingEnergyKwh ) ,
146+ pausedEnergyKwh : r6 ( result . pausedEnergyKwh ) ,
147+ checkpointEnergyKwh : r6 ( result . checkpointEnergyKwh ) ,
148+ totalEmissionsKgco2 : r6 ( result . totalEmissionsKgco2 ) ,
149+ tokensProcessed : result . tokensProcessed ,
150+ tokensTotal : result . tokensTotal ,
151+ completed : result . completed ,
152+ numPauses : result . numPauses ,
153+ actualOverheadPct : r6 ( result . actualOverheadPct ) ,
154+ withinOverheadBudget : result . withinOverheadBudget ,
155+ baselineEmissionsKgco2 : r6 ( result . baselineEmissionsKgco2 ) ,
156+ baselineTimeH : r6 ( result . baselineTimeH ) ,
157+ co2SavingsPct : r6 ( result . co2SavingsPct ) ,
158+ score : r6 ( result . score ) ,
159+ idleTimeH : r6 ( result . idleTimeH ) ,
160+ completionPct : r6 ( result . completionPct ) ,
161+ ok : result . ok ,
162+ stopReason : result . stopReason ,
163+ issues : result . issues . join ( "; " ) ,
171164 } ) ;
172165
173166 if ( ! skipLive ) {
@@ -177,7 +170,6 @@ async function main() {
177170 }
178171 }
179172
180- // Summary
181173 console . log ( `\n\n ─────────────────────────────────────────────` ) ;
182174 console . log ( ` Results: ${ results . length } runs · ✓ ${ ok } · ✗ ${ fail } ` ) ;
183175 console . log ( ` ─────────────────────────────────────────────` ) ;
0 commit comments