Skip to content

Commit c1ea46f

Browse files
committed
refactor: use webworkers
1 parent bce4197 commit c1ea46f

23 files changed

Lines changed: 1783 additions & 986 deletions

TheGreenEpochWeb/src/cli/index.ts

Lines changed: 58 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
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-
121
import { readFileSync, writeFileSync } from "node:fs";
132
import { resolve, dirname } from "node:path";
143
import { 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

1810
const __dirname = dirname(fileURLToPath(import.meta.url));
1911
const 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+
7872
async 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(` ─────────────────────────────────────────────`);
Lines changed: 4 additions & 211 deletions
Original file line numberDiff line numberDiff line change
@@ -1,211 +1,4 @@
1-
import type { FullProfile, SimConfig, CO2Timeline, Scenario } from "../types";
2-
import { simulateStepwise, buildResult } from "./simulation";
3-
4-
export interface SweepPoint {
5-
thetaPause: number;
6-
thetaResume: number;
7-
actualOverheadPct: number;
8-
co2SavingsPct: number;
9-
score: number;
10-
numPauses: number;
11-
totalEmissionsKgco2: number;
12-
baselineEmissionsKgco2: number;
13-
withinBudget: boolean;
14-
stopReason: string;
15-
completed: boolean;
16-
iteration: number;
17-
}
18-
19-
export interface SweepOptions {
20-
thetaPauseMin: number;
21-
thetaPauseMax: number;
22-
thetaPauseStep: number;
23-
hysteresisMode: "ratio" | "offset";
24-
hysteresisValue: number;
25-
overheadBudgetPct: number;
26-
}
27-
28-
export interface AdaptiveOptions {
29-
thetaPauseMax: number;
30-
overheadBudgetPct: number;
31-
resolution: number;
32-
maxIterations: number;
33-
minStep: number;
34-
shrinkFactor: number;
35-
}
36-
37-
export interface AdaptiveResult {
38-
points: SweepPoint[];
39-
best: SweepPoint | null;
40-
iterations: number;
41-
}
42-
43-
export function runSweep(
44-
profile: FullProfile,
45-
timeline: CO2Timeline,
46-
scenario: Scenario,
47-
options: SweepOptions,
48-
startTimeIdx = 0,
49-
): SweepPoint[] {
50-
const startTime = scenario.startTimes[startTimeIdx] || "01-01";
51-
52-
const shared = {
53-
scenarioDescription: scenario.description,
54-
region: scenario.region,
55-
historicalYears: scenario.historicalYears,
56-
startTime,
57-
overheadBudgetPct: options.overheadBudgetPct,
58-
};
59-
60-
const baselineLast = runBaseline(profile, shared, timeline);
61-
62-
const points: SweepPoint[] = [];
63-
for (let tp = options.thetaPauseMin; tp <= options.thetaPauseMax; tp = round(tp + options.thetaPauseStep)) {
64-
const tr = options.hysteresisMode === "ratio"
65-
? round(tp * options.hysteresisValue)
66-
: Math.max(0, round(tp - options.hysteresisValue));
67-
68-
const pt = evaluate(profile, { ...shared, thetaPause: tp, thetaResume: tr }, timeline, baselineLast, 0);
69-
if (pt) points.push(pt);
70-
}
71-
72-
return points;
73-
}
74-
75-
export function adaptiveSweep(
76-
profile: FullProfile,
77-
timeline: CO2Timeline,
78-
scenario: Scenario,
79-
options: AdaptiveOptions,
80-
startTimeIdx = 0,
81-
onIteration?: (iteration: number, points: SweepPoint[], best: SweepPoint | null) => void,
82-
): AdaptiveResult {
83-
const startTime = scenario.startTimes[startTimeIdx] || "01-01";
84-
85-
const shared = {
86-
scenarioDescription: scenario.description,
87-
region: scenario.region,
88-
historicalYears: scenario.historicalYears,
89-
startTime,
90-
overheadBudgetPct: options.overheadBudgetPct,
91-
};
92-
93-
const baselineLast = runBaseline(profile, shared, timeline);
94-
95-
const allPoints: SweepPoint[] = [];
96-
let iterPoints: SweepPoint[] = [];
97-
let best: SweepPoint | null = null;
98-
99-
let tpMin = 10;
100-
let tpMax = options.thetaPauseMax;
101-
let trMin = 0;
102-
let trMax = options.thetaPauseMax;
103-
104-
for (let iter = 0; iter < options.maxIterations; iter++) {
105-
const stepTp = Math.max((tpMax - tpMin) / (options.resolution - 1), 1);
106-
const stepTr = Math.max((trMax - trMin) / (options.resolution - 1), 1);
107-
108-
if (stepTp < options.minStep && stepTr < options.minStep) break;
109-
110-
iterPoints = [];
111-
112-
for (let tpi = 0; tpi < options.resolution; tpi++) {
113-
const tp = round(tpMin + tpi * stepTp);
114-
if (tp > options.thetaPauseMax) break;
115-
116-
const trCount = Math.max(2, Math.round(((tp - trMin) / (trMax - trMin)) * options.resolution));
117-
for (let tri = 0; tri < trCount; tri++) {
118-
const tr = round(trMin + tri * (tp - trMin) / Math.max(trCount - 1, 1));
119-
if (tr > tp) break;
120-
121-
const pt = evaluate(profile, { ...shared, thetaPause: tp, thetaResume: tr }, timeline, baselineLast, iter);
122-
if (pt) iterPoints.push(pt);
123-
}
124-
}
125-
126-
allPoints.push(...iterPoints);
127-
128-
const iterBest = findBest(iterPoints, options.overheadBudgetPct);
129-
if (iterBest && (!best || iterBest.score > best.score)) {
130-
best = iterBest;
131-
}
132-
133-
if (onIteration) onIteration(iter, iterPoints, best);
134-
135-
if (iter === options.maxIterations - 1) break;
136-
137-
if (!best) {
138-
tpMin = Math.max(10, tpMin * 0.8);
139-
tpMax = Math.min(options.thetaPauseMax, tpMax * 0.9);
140-
trMin = 0;
141-
trMax = tpMax;
142-
continue;
143-
}
144-
145-
const span = tpMax - tpMin;
146-
const newMin = Math.max(10, best.thetaPause - span * options.shrinkFactor / 2);
147-
const newMax = Math.min(options.thetaPauseMax, best.thetaPause + span * options.shrinkFactor / 2);
148-
149-
tpMin = round(newMin);
150-
tpMax = round(newMax);
151-
trMin = round(Math.max(0, best.thetaResume - span * options.shrinkFactor / 2));
152-
trMax = round(Math.min(tpMax, best.thetaResume + span * options.shrinkFactor / 2));
153-
trMin = Math.max(0, trMin);
154-
trMax = Math.max(trMin + options.minStep, trMax);
155-
}
156-
157-
return { points: allPoints, best, iterations: Math.min(options.maxIterations, allPoints.length > 0 ? allPoints[allPoints.length - 1].iteration + 1 : 0) };
158-
}
159-
160-
function runBaseline(profile: FullProfile, shared: { scenarioDescription: string; region: string; historicalYears: number[]; startTime: string; overheadBudgetPct: number }, timeline: CO2Timeline) {
161-
const baselineProgress: import("../types").SimProgress[] = [];
162-
for (const p of simulateStepwise(profile, { ...shared, thetaPause: Infinity, thetaResume: 0 }, timeline)) {
163-
baselineProgress.push(p);
164-
}
165-
return baselineProgress[baselineProgress.length - 1];
166-
}
167-
168-
function evaluate(
169-
profile: FullProfile,
170-
config: SimConfig,
171-
timeline: CO2Timeline,
172-
baselineLast: import("../types").SimProgress,
173-
iteration: number,
174-
): SweepPoint | null {
175-
let lastProgress: import("../types").SimProgress | null = null;
176-
for (const p of simulateStepwise(profile, config, timeline)) {
177-
lastProgress = p;
178-
}
179-
if (!lastProgress) return null;
180-
181-
const meta = buildResult(profile, config, lastProgress, baselineLast);
182-
const actualOverheadPct = meta.actualOverheadPct;
183-
const co2SavingsPct = meta.baselineEmissionsKgco2 > 0
184-
? (meta.baselineEmissionsKgco2 - meta.totalEmissionsKgco2) / meta.baselineEmissionsKgco2 * 100
185-
: 0;
186-
187-
return {
188-
thetaPause: config.thetaPause === Infinity ? 9999 : config.thetaPause,
189-
thetaResume: config.thetaResume,
190-
actualOverheadPct,
191-
co2SavingsPct,
192-
score: co2SavingsPct / Math.max(actualOverheadPct, 0.001),
193-
numPauses: meta.numPauses,
194-
totalEmissionsKgco2: meta.totalEmissionsKgco2,
195-
baselineEmissionsKgco2: meta.baselineEmissionsKgco2,
196-
withinBudget: meta.withinOverheadBudget,
197-
stopReason: meta.stopReason,
198-
completed: meta.completed,
199-
iteration,
200-
};
201-
}
202-
203-
function findBest(points: SweepPoint[], budget: number): SweepPoint | null {
204-
const valid = points.filter(r => r.withinBudget && r.co2SavingsPct > 0);
205-
if (valid.length === 0) return null;
206-
return valid.reduce((a, b) => (a.score > b.score ? a : b));
207-
}
208-
209-
function round(n: number): number {
210-
return Math.round(n * 100) / 100;
211-
}
1+
export { runOptimization as adaptiveSweep, runBaseline, evaluatePoint } from "../engine/runner";
2+
export { runOptimizationInWorker } from "../engine/worker";
3+
export type { SweepPoint } from "../domain/types";
4+
export type { AdaptiveOptions as SweepOptions } from "../engine/runner";

0 commit comments

Comments
 (0)