@openagentsy/footprint 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,35 @@
1
- # Temporary Holding Version
1
+ # @openagentsy/footprint
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The energy, water and carbon of one inference, as ranges.
4
+
5
+ Every quantity this package produces is an interval, because the inputs are:
6
+ a model size nobody discloses, a data centre's PUE, a grid's carbon factor.
7
+ A point estimate would hide that, so the type makes the width impossible to
8
+ lose.
9
+
10
+ The formulas are re-implemented from the EcoLogits LLM inference
11
+ methodology (CC BY-SA 4.0, https://github.com/mlco2/ecologits). The EcoLogits
12
+ data is not bundled here: its license is unverified, so the model map and the
13
+ grid factors are injected by the caller.
14
+
15
+ ```ts
16
+ import { estimateRemote } from '@openagentsy/footprint';
17
+
18
+ const impact = estimateRemote(usage, arch, site, throughput);
19
+ impact.energyWh; // [lo, hi] in Wh
20
+ impact.co2Kg; // [lo, hi] in kgCO2
21
+ ```
22
+
23
+ The prefill term is an OpenAgentsy extension and it is off by default. Its
24
+ coefficients are fitted from ML.ENERGY measurements and committed in
25
+ `data/prefill-fit.json`, but they do not ship enabled: `footprint.prefill`
26
+ stays false, and a caller who wants the term passes the coefficients in.
27
+
28
+ ```ts
29
+ const impact = estimateRemote(usage, arch, site, throughput, {
30
+ prefill: { kIn: [7.19e-7, 7.19e-7], kCache: [7.19e-8, 7.19e-8] },
31
+ });
32
+ ```
33
+
34
+ The golden data in `data/golden.json` was computed with the EcoLogits
35
+ library, offline, by `scripts/golden.py`.
@@ -0,0 +1,27 @@
1
+ import type { Range } from './types.js';
2
+ /** Wh per token per billion active parameters, at the batch factor. */
3
+ export declare const ALPHA = 0.00000117;
4
+ /** The exponent of the batch factor in the GPU energy regression. */
5
+ export declare const BETA = -0.0112;
6
+ /** Wh per token, the part that does not scale with the model. */
7
+ export declare const GAMMA = 0.0000405;
8
+ /** Requests the server handles at once, the amortisation factor. */
9
+ export declare const BATCH = 64;
10
+ /** kW of the rest of the server, the part that is not a GPU. */
11
+ export declare const W_SERVER_NO_GPU_KW = 1.2;
12
+ /** GPUs in one server. */
13
+ export declare const GPUS_INSTALLED = 8;
14
+ /** GB of memory on one GPU (an H100 80GB). */
15
+ export declare const GPU_MEM_GB = 80;
16
+ /** Bits per weight of the quantised model. */
17
+ export declare const QUANT_BITS = 16;
18
+ /** Memory overhead of loading a model, over its raw weights. */
19
+ export declare const MEM_OVERHEAD = 1.2;
20
+ /**
21
+ * IPCC AR6 WG1 SPM D.1.1: 0.45 degrees Celsius per 1000 GtCO2, likely
22
+ * between 0.27 and 0.63. 1000 Gt is 1e15 kg.
23
+ */
24
+ export declare const TCRE_C_PER_KG: Range;
25
+ /** The central estimate of the same relation. */
26
+ export declare const TCRE_BEST_C_PER_KG = 4.5e-16;
27
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAMxC,uEAAuE;AACvE,eAAO,MAAM,KAAK,aAAU,CAAC;AAC7B,qEAAqE;AACrE,eAAO,MAAM,IAAI,UAAW,CAAC;AAC7B,iEAAiE;AACjE,eAAO,MAAM,KAAK,YAAU,CAAC;AAC7B,oEAAoE;AACpE,eAAO,MAAM,KAAK,KAAK,CAAC;AACxB,gEAAgE;AAChE,eAAO,MAAM,kBAAkB,MAAM,CAAC;AACtC,0BAA0B;AAC1B,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,8CAA8C;AAC9C,eAAO,MAAM,UAAU,KAAK,CAAC;AAC7B,8CAA8C;AAC9C,eAAO,MAAM,UAAU,KAAK,CAAC;AAC7B,gEAAgE;AAChE,eAAO,MAAM,YAAY,MAAM,CAAC;AAEhC;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,KAA4B,CAAC;AACzD,iDAAiD;AACjD,eAAO,MAAM,kBAAkB,UAAW,CAAC"}
@@ -0,0 +1,29 @@
1
+ // EcoLogits LLM inference methodology (CC BY-SA 4.0). https://github.com/mlco2/ecologits
2
+ // The formulas below are re-implemented from that methodology; the data
3
+ // behind them is not bundled here. See data/SOURCES.md.
4
+ /** Wh per token per billion active parameters, at the batch factor. */
5
+ export const ALPHA = 1.17e-6;
6
+ /** The exponent of the batch factor in the GPU energy regression. */
7
+ export const BETA = -1.12e-2;
8
+ /** Wh per token, the part that does not scale with the model. */
9
+ export const GAMMA = 4.05e-5;
10
+ /** Requests the server handles at once, the amortisation factor. */
11
+ export const BATCH = 64;
12
+ /** kW of the rest of the server, the part that is not a GPU. */
13
+ export const W_SERVER_NO_GPU_KW = 1.2;
14
+ /** GPUs in one server. */
15
+ export const GPUS_INSTALLED = 8;
16
+ /** GB of memory on one GPU (an H100 80GB). */
17
+ export const GPU_MEM_GB = 80;
18
+ /** Bits per weight of the quantised model. */
19
+ export const QUANT_BITS = 16;
20
+ /** Memory overhead of loading a model, over its raw weights. */
21
+ export const MEM_OVERHEAD = 1.2;
22
+ /**
23
+ * IPCC AR6 WG1 SPM D.1.1: 0.45 degrees Celsius per 1000 GtCO2, likely
24
+ * between 0.27 and 0.63. 1000 Gt is 1e15 kg.
25
+ */
26
+ export const TCRE_C_PER_KG = [0.27e-15, 0.63e-15];
27
+ /** The central estimate of the same relation. */
28
+ export const TCRE_BEST_C_PER_KG = 0.45e-15;
29
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAEA,yFAAyF;AACzF,wEAAwE;AACxE,wDAAwD;AAExD,uEAAuE;AACvE,MAAM,CAAC,MAAM,KAAK,GAAG,OAAO,CAAC;AAC7B,qEAAqE;AACrE,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC;AAC7B,iEAAiE;AACjE,MAAM,CAAC,MAAM,KAAK,GAAG,OAAO,CAAC;AAC7B,oEAAoE;AACpE,MAAM,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;AACxB,gEAAgE;AAChE,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AACtC,0BAA0B;AAC1B,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAChC,8CAA8C;AAC9C,MAAM,CAAC,MAAM,UAAU,GAAG,EAAE,CAAC;AAC7B,8CAA8C;AAC9C,MAAM,CAAC,MAAM,UAAU,GAAG,EAAE,CAAC;AAC7B,gEAAgE;AAChE,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAC;AAEhC;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAU,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AACzD,iDAAiD;AACjD,MAAM,CAAC,MAAM,kBAAkB,GAAG,QAAQ,CAAC"}
@@ -0,0 +1,54 @@
1
+ import { type Impact, type ModelArch, type PrefillCoeff, type Range, type SiteProfile, type Throughput, type Usage } from './types.js';
2
+ /**
3
+ * The energy of one output token on one GPU, in Wh.
4
+ *
5
+ * The EcoLogits regression: a part that scales with the active parameters of
6
+ * the model and a part that does not, both divided by the number of requests
7
+ * the server handles at once.
8
+ */
9
+ export declare const energyPerToken: (activeParamsB: number) => number;
10
+ /**
11
+ * How many GPUs the model needs to sit in memory.
12
+ *
13
+ * The count is monotone in the model size, and it doubles rather than
14
+ * creeping, because a server holds eight GPUs and a model is not split
15
+ * across a fraction of one.
16
+ */
17
+ export declare function gpuCount(totalParamsB: number): number;
18
+ /** Where the energy of a request went. */
19
+ export interface RemoteBreakdown {
20
+ /** What the GPUs spent. */
21
+ gpuWh: Range;
22
+ /** What the rest of the server spent, over the generation time. */
23
+ restWh: Range;
24
+ /** The IT energy, before the data centre overhead and the prefill term. */
25
+ serverWh: Range;
26
+ }
27
+ /**
28
+ * The energy of a request against a remote provider, before the data centre.
29
+ *
30
+ * The GPU count and the per-token energy are both intervals, so the estimate
31
+ * is an interval too. The bounds come from the corners of those intervals,
32
+ * which is where the energy is largest and smallest.
33
+ */
34
+ export declare function remoteEnergyBreakdown(usage: Usage, arch: ModelArch, throughput: Throughput, opts?: {
35
+ prefill?: PrefillCoeff;
36
+ }): RemoteBreakdown;
37
+ /**
38
+ * The impact of a request against a remote provider.
39
+ *
40
+ * The energy is the data centre's, so the PUE is applied to it, and the
41
+ * water is the cooling inside the data centre plus the water embedded in
42
+ * the electricity that powered it.
43
+ */
44
+ export declare function estimateRemote(usage: Usage, arch: ModelArch, site: SiteProfile, throughput: Throughput, opts?: {
45
+ prefill?: PrefillCoeff;
46
+ }): Impact;
47
+ /**
48
+ * The impact of a request against a local runtime (Ollama, LM Studio).
49
+ *
50
+ * The time and the power of the device are the whole estimate, and there is
51
+ * no data centre, so there is no cooling water.
52
+ */
53
+ export declare function estimateLocal(seconds: number, deviceWatts: Range, grid: SiteProfile): Impact;
54
+ //# sourceMappingURL=estimate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"estimate.d.ts","sourceRoot":"","sources":["../src/estimate.ts"],"names":[],"mappings":"AAYA,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,SAAS,EAEd,KAAK,YAAY,EACjB,KAAK,KAAK,EAEV,KAAK,WAAW,EAEhB,KAAK,UAAU,EACf,KAAK,KAAK,EACX,MAAM,YAAY,CAAC;AAEpB;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,eAAe,MAAM,KAAG,MACC,CAAC;AAEzD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAIrD;AAED,0CAA0C;AAC1C,MAAM,WAAW,eAAe;IAC9B,2BAA2B;IAC3B,KAAK,EAAE,KAAK,CAAC;IACb,mEAAmE;IACnE,MAAM,EAAE,KAAK,CAAC;IACd,2EAA2E;IAC3E,QAAQ,EAAE,KAAK,CAAC;CACjB;AASD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,SAAS,EACf,UAAU,EAAE,UAAU,EACtB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,YAAY,CAAA;CAAO,GACpC,eAAe,CAuBjB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,WAAW,EACjB,UAAU,EAAE,UAAU,EACtB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,YAAY,CAAA;CAAO,GACpC,MAAM,CAwBR;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAW5F"}
@@ -0,0 +1,114 @@
1
+ import { ALPHA, BATCH, BETA, GAMMA, GPU_MEM_GB, GPUS_INSTALLED, MEM_OVERHEAD, QUANT_BITS, TCRE_C_PER_KG, W_SERVER_NO_GPU_KW, } from './constants.js';
2
+ import { add, mul, r, scale, } from './types.js';
3
+ /**
4
+ * The energy of one output token on one GPU, in Wh.
5
+ *
6
+ * The EcoLogits regression: a part that scales with the active parameters of
7
+ * the model and a part that does not, both divided by the number of requests
8
+ * the server handles at once.
9
+ */
10
+ export const energyPerToken = (activeParamsB) => ALPHA * Math.exp(BETA * BATCH) * activeParamsB + GAMMA;
11
+ /**
12
+ * How many GPUs the model needs to sit in memory.
13
+ *
14
+ * The count is monotone in the model size, and it doubles rather than
15
+ * creeping, because a server holds eight GPUs and a model is not split
16
+ * across a fraction of one.
17
+ */
18
+ export function gpuCount(totalParamsB) {
19
+ const memGB = (MEM_OVERHEAD * totalParamsB * 1e9 * QUANT_BITS) / 8 / 1e9;
20
+ const needed = Math.max(1, Math.ceil(memGB / GPU_MEM_GB));
21
+ return 2 ** Math.ceil(Math.log2(needed));
22
+ }
23
+ /** The prefill term is an OpenAgentsy extension, and it is off by default. */
24
+ function prefillWh(usage, arch, coeff) {
25
+ const fresh = mul(mul(arch.activeParamsB, coeff.kIn), r(usage.inputTokens));
26
+ const cached = mul(mul(arch.activeParamsB, coeff.kCache), r(usage.cacheReadTokens));
27
+ return add(fresh, cached);
28
+ }
29
+ /**
30
+ * The energy of a request against a remote provider, before the data centre.
31
+ *
32
+ * The GPU count and the per-token energy are both intervals, so the estimate
33
+ * is an interval too. The bounds come from the corners of those intervals,
34
+ * which is where the energy is largest and smallest.
35
+ */
36
+ export function remoteEnergyBreakdown(usage, arch, throughput, opts = {}) {
37
+ const gpus = r(gpuCount(arch.totalParamsB[0]), gpuCount(arch.totalParamsB[1]));
38
+ const perToken = r(energyPerToken(arch.activeParamsB[0]), energyPerToken(arch.activeParamsB[1]));
39
+ const gpuWh = mul(scale(perToken, usage.outputTokens), gpus);
40
+ const generation = r(throughput.ttftS[0] + usage.outputTokens / throughput.tps[1], throughput.ttftS[1] + usage.outputTokens / throughput.tps[0]);
41
+ const dt = usage.latencyS
42
+ ? r(Math.min(generation[0], usage.latencyS), Math.min(generation[1], usage.latencyS))
43
+ : generation;
44
+ const restWh = scale(mul(dt, scale(gpus, 1 / GPUS_INSTALLED)), (W_SERVER_NO_GPU_KW * 1000) / 3600 / BATCH);
45
+ let serverWh = add(gpuWh, restWh);
46
+ if (opts.prefill) {
47
+ serverWh = add(serverWh, prefillWh(usage, arch, opts.prefill));
48
+ }
49
+ return { gpuWh, restWh, serverWh };
50
+ }
51
+ /**
52
+ * The impact of a request against a remote provider.
53
+ *
54
+ * The energy is the data centre's, so the PUE is applied to it, and the
55
+ * water is the cooling inside the data centre plus the water embedded in
56
+ * the electricity that powered it.
57
+ */
58
+ export function estimateRemote(usage, arch, site, throughput, opts = {}) {
59
+ const notes = ['output tokens only (EcoLogits method)'];
60
+ const { serverWh: eServer } = remoteEnergyBreakdown(usage, arch, throughput, opts);
61
+ if (opts.prefill) {
62
+ notes.push('prefill term on (OpenAgentsy extension, not EcoLogits)');
63
+ }
64
+ const energyWh = mul(eServer, site.pue);
65
+ const kWhServer = scale(eServer, 1 / 1000);
66
+ const waterL = add(mul(kWhServer, site.wueOnSite), mul(mul(kWhServer, site.pue), site.wueOffSite));
67
+ const co2Kg = mul(scale(energyWh, 1 / 1000), site.gridKgCo2PerKWh);
68
+ const warmingC = mul(co2Kg, TCRE_C_PER_KG);
69
+ return {
70
+ energyWh,
71
+ waterL,
72
+ co2Kg,
73
+ warmingC,
74
+ dominantUncertainty: dominant(arch, site),
75
+ notes,
76
+ };
77
+ }
78
+ /**
79
+ * The impact of a request against a local runtime (Ollama, LM Studio).
80
+ *
81
+ * The time and the power of the device are the whole estimate, and there is
82
+ * no data centre, so there is no cooling water.
83
+ */
84
+ export function estimateLocal(seconds, deviceWatts, grid) {
85
+ const energyWh = scale(deviceWatts, seconds / 3600);
86
+ const kWh = scale(energyWh, 1 / 1000);
87
+ return {
88
+ energyWh,
89
+ waterL: mul(kWh, grid.wueOffSite),
90
+ co2Kg: mul(kWh, grid.gridKgCo2PerKWh),
91
+ warmingC: mul(mul(kWh, grid.gridKgCo2PerKWh), TCRE_C_PER_KG),
92
+ dominantUncertainty: 'grid',
93
+ notes: ['local device estimate: configured watts x generation time'],
94
+ };
95
+ }
96
+ /**
97
+ * Which input the answer leans on hardest.
98
+ *
99
+ * The width of an interval, relative to its low end, is what decides this.
100
+ * The three inputs that can widen the answer are the model size, the site
101
+ * and the grid.
102
+ */
103
+ function dominant(arch, site) {
104
+ const width = (x) => (x[1] - x[0]) / Math.max(x[0], 1e-12);
105
+ const scores = {
106
+ 'model-size': width(arch.totalParamsB),
107
+ site: width(site.pue) + width(site.wueOnSite),
108
+ grid: width(site.gridKgCo2PerKWh),
109
+ };
110
+ const ranked = Object.entries(scores).sort((a, b) => b[1] - a[1]);
111
+ const top = ranked[0];
112
+ return (top ? top[0] : 'model-size');
113
+ }
114
+ //# sourceMappingURL=estimate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"estimate.js","sourceRoot":"","sources":["../src/estimate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=estimate.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"estimate.test.d.ts","sourceRoot":"","sources":["../src/estimate.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,136 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { TCRE_BEST_C_PER_KG } from './constants.js';
3
+ import { energyPerToken, estimateLocal, estimateRemote, gpuCount, remoteEnergyBreakdown, } from './estimate.js';
4
+ import { r } from './types.js';
5
+ /** The plan's numbers are rounded, so a relative tolerance is the honest one. */
6
+ function expectClose(actual, expected, tolerance) {
7
+ expect(Math.abs(actual - expected) / Math.abs(expected)).toBeLessThan(tolerance);
8
+ }
9
+ /** The worked example of the plan: a 300B model, 150k tokens, a 0.4 grid. */
10
+ const USAGE = {
11
+ outputTokens: 150_000,
12
+ inputTokens: 0,
13
+ cacheReadTokens: 0,
14
+ cacheWriteTokens: 0,
15
+ };
16
+ const ARCH = {
17
+ totalParamsB: r(300),
18
+ activeParamsB: r(30),
19
+ source: 'user',
20
+ };
21
+ const SITE = {
22
+ pue: r(1.2),
23
+ wueOnSite: r(0.5),
24
+ wueOffSite: r(1.6),
25
+ gridKgCo2PerKWh: r(0.4),
26
+ source: 'test',
27
+ disclosed: true,
28
+ };
29
+ const THROUGHPUT = { ttftS: r(0), tps: r(60) };
30
+ /** A generator that repeats, so a failure is a failure and not a bad day. */
31
+ function makeRandom(seed) {
32
+ let state = seed >>> 0;
33
+ return () => {
34
+ state = (state + 0x6d2b79f5) >>> 0;
35
+ let t = state;
36
+ t = Math.imul(t ^ (t >>> 15), t | 1);
37
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
38
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
39
+ };
40
+ }
41
+ describe('the footprint estimator', () => {
42
+ it('reproduces the worked example of the plan', () => {
43
+ expect(gpuCount(300)).toBe(16);
44
+ expectClose(energyPerToken(30), 5.76e-5, 2e-3);
45
+ const breakdown = remoteEnergyBreakdown(USAGE, ARCH, THROUGHPUT);
46
+ expectClose(breakdown.gpuWh[0], 138.3, 1e-3);
47
+ expectClose(breakdown.restWh[0], 26.0, 2.5e-3);
48
+ const impact = estimateRemote(USAGE, ARCH, SITE, THROUGHPUT);
49
+ expectClose(impact.energyWh[0], 197.2, 1e-3);
50
+ expectClose(impact.co2Kg[0] * 1000, 78.9, 1e-3);
51
+ expectClose(impact.co2Kg[0] * TCRE_BEST_C_PER_KG, 3.55e-17, 1e-3);
52
+ });
53
+ it('counts the output tokens only, and says so', () => {
54
+ const impact = estimateRemote(USAGE, ARCH, SITE, THROUGHPUT);
55
+ expect(impact.notes).toContain('output tokens only (EcoLogits method)');
56
+ expect(impact.notes).not.toContain('prefill term on (OpenAgentsy extension, not EcoLogits)');
57
+ });
58
+ it('adds the prefill term only when it is asked for', () => {
59
+ const usage = { ...USAGE, inputTokens: 2000, cacheReadTokens: 500 };
60
+ const without = estimateRemote(usage, ARCH, SITE, THROUGHPUT);
61
+ const with_ = estimateRemote(usage, ARCH, SITE, THROUGHPUT, {
62
+ prefill: { kIn: r(1e-8), kCache: r(1e-9) },
63
+ });
64
+ expect(with_.energyWh[0]).toBeGreaterThan(without.energyWh[0]);
65
+ expect(with_.notes).toContain('prefill term on (OpenAgentsy extension, not EcoLogits)');
66
+ });
67
+ it('caps the generation time at the measured latency', () => {
68
+ const slow = { ...USAGE, latencyS: 10 };
69
+ const breakdown = remoteEnergyBreakdown(slow, ARCH, THROUGHPUT);
70
+ expect(breakdown.restWh[1]).toBeLessThan(remoteEnergyBreakdown(USAGE, ARCH, THROUGHPUT).restWh[0]);
71
+ });
72
+ it('counts no data centre water for a local run', () => {
73
+ const impact = estimateLocal(60, r(25, 60), SITE);
74
+ // 25 W for 60 s is 0.4167 Wh, which is 0.0004167 kWh of electricity.
75
+ expect(impact.waterL[0]).toBeCloseTo(((25 * 60) / 3600 / 1000) * 1.6, 12);
76
+ expect(impact.dominantUncertainty).toBe('grid');
77
+ });
78
+ it('keeps every bound in order, whatever the input', () => {
79
+ const random = makeRandom(20261026);
80
+ for (let i = 0; i < 200; i += 1) {
81
+ const arch = {
82
+ totalParamsB: r(random() * 500, random() * 900),
83
+ activeParamsB: r(random() * 80, random() * 120),
84
+ source: 'user',
85
+ };
86
+ const site = {
87
+ pue: r(1 + random() * 0.2, 1 + random() * 0.4),
88
+ wueOnSite: r(random() * 0.2, random() * 1.4),
89
+ wueOffSite: r(random() * 0.5, random() * 2),
90
+ gridKgCo2PerKWh: r(random() * 0.1, random() * 0.9),
91
+ source: 'test',
92
+ disclosed: false,
93
+ };
94
+ const usage = {
95
+ outputTokens: Math.floor(random() * 5000),
96
+ inputTokens: Math.floor(random() * 2000),
97
+ cacheReadTokens: Math.floor(random() * 2000),
98
+ cacheWriteTokens: 0,
99
+ };
100
+ const throughput = {
101
+ ttftS: r(random() * 0.5, random() * 1.5),
102
+ tps: r(5 + random() * 100, 10 + random() * 200),
103
+ };
104
+ const impact = estimateRemote(usage, arch, site, throughput);
105
+ for (const range of [impact.energyWh, impact.waterL, impact.co2Kg, impact.warmingC]) {
106
+ expect(range[0]).toBeLessThanOrEqual(range[1]);
107
+ }
108
+ }
109
+ });
110
+ it('never spends less energy for more tokens', () => {
111
+ const random = makeRandom(7);
112
+ for (let i = 0; i < 50; i += 1) {
113
+ const arch = {
114
+ totalParamsB: r(10, 10 + random() * 400),
115
+ activeParamsB: r(2, 2 + random() * 90),
116
+ source: 'user',
117
+ };
118
+ const throughput = { ttftS: r(0, random() * 0.5), tps: r(20, 90) };
119
+ const small = estimateRemote({ outputTokens: 100, inputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }, arch, SITE, throughput);
120
+ const large = estimateRemote({ outputTokens: 2000, inputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }, arch, SITE, throughput);
121
+ expect(large.energyWh[0]).toBeGreaterThanOrEqual(small.energyWh[0]);
122
+ expect(large.energyWh[1]).toBeGreaterThanOrEqual(small.energyWh[1]);
123
+ }
124
+ });
125
+ it('never needs fewer GPUs for a bigger model', () => {
126
+ const random = makeRandom(31);
127
+ const sizes = Array.from({ length: 500 }, () => random() * 2000).sort((a, b) => a - b);
128
+ let previous = gpuCount(sizes[0] ?? 0);
129
+ for (const size of sizes) {
130
+ const count = gpuCount(size);
131
+ expect(count).toBeGreaterThanOrEqual(previous);
132
+ previous = count;
133
+ }
134
+ });
135
+ });
136
+ //# sourceMappingURL=estimate.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"estimate.test.js","sourceRoot":"","sources":["../src/estimate.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=golden.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"golden.test.d.ts","sourceRoot":"","sources":["../src/golden.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,55 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { estimateRemote } from './estimate.js';
5
+ import { r } from './types.js';
6
+ const goldenPath = fileURLToPath(new URL('../data/golden.json', import.meta.url));
7
+ const present = existsSync(goldenPath);
8
+ function readEntries() {
9
+ const parsed = JSON.parse(readFileSync(goldenPath, 'utf8'));
10
+ if (typeof parsed !== 'object' || parsed === null)
11
+ return [];
12
+ const entries = parsed.entries;
13
+ return Array.isArray(entries) ? entries : [];
14
+ }
15
+ const entries = present ? readEntries() : [];
16
+ function mid(range) {
17
+ return (range[0] + range[1]) / 2;
18
+ }
19
+ function expectClose(actual, expected, tolerance) {
20
+ expect(Math.abs(actual - expected) / Math.abs(expected)).toBeLessThan(tolerance);
21
+ }
22
+ describe.skipIf(!present)('the golden estimates', () => {
23
+ it('holds twenty fixed pairs', () => {
24
+ expect(entries).toHaveLength(20);
25
+ });
26
+ for (const entry of entries) {
27
+ it(`matches the methodology for ${entry.provider}/${entry.model}`, () => {
28
+ const arch = {
29
+ totalParamsB: r(entry.paramsB),
30
+ activeParamsB: r(entry.paramsB),
31
+ source: 'ecologits',
32
+ };
33
+ const site = {
34
+ pue: r(entry.pue[0], entry.pue[1]),
35
+ wueOnSite: r(entry.wueOnSite[0], entry.wueOnSite[1]),
36
+ wueOffSite: r(entry.gridWueLPerKWh),
37
+ gridKgCo2PerKWh: r(entry.gridKgCo2PerKWh),
38
+ source: `ecologits:${entry.provider}`,
39
+ disclosed: true,
40
+ };
41
+ const usage = {
42
+ outputTokens: entry.outputTokens,
43
+ inputTokens: 0,
44
+ cacheReadTokens: 0,
45
+ cacheWriteTokens: 0,
46
+ };
47
+ const throughput = { ttftS: r(entry.ttftS), tps: r(entry.tps) };
48
+ const impact = estimateRemote(usage, arch, site, throughput);
49
+ expectClose(mid(impact.energyWh) / 1000, entry.energyKWh, 0.01);
50
+ expectClose(mid(impact.co2Kg), entry.co2Kg, 0.01);
51
+ expectClose(mid(impact.waterL), entry.waterL, 0.01);
52
+ });
53
+ }
54
+ });
55
+ //# sourceMappingURL=golden.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"golden.test.js","sourceRoot":"","sources":["../src/golden.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export * from './constants.js';
2
+ export * from './estimate.js';
3
+ export * from './resolve.js';
4
+ export * from './types.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from './constants.js';
2
+ export * from './estimate.js';
3
+ export * from './resolve.js';
4
+ export * from './types.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=prefill.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefill.test.d.ts","sourceRoot":"","sources":["../src/prefill.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,76 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { estimateRemote, remoteEnergyBreakdown } from './estimate.js';
4
+ import { r } from './types.js';
5
+ /** The fit the derivation script wrote. The tests use it, so it cannot drift. */
6
+ const FIT = JSON.parse(readFileSync(new URL('../data/prefill-fit.json', import.meta.url), 'utf8'));
7
+ const ARCH = { totalParamsB: r(300), activeParamsB: r(30), source: 'user' };
8
+ const SITE = {
9
+ pue: r(1.2),
10
+ wueOnSite: r(0.5),
11
+ wueOffSite: r(1.6),
12
+ gridKgCo2PerKWh: r(0.4),
13
+ source: 'test',
14
+ disclosed: true,
15
+ };
16
+ const THROUGHPUT = { ttftS: r(0), tps: r(60) };
17
+ const USAGE = {
18
+ outputTokens: 0,
19
+ inputTokens: 4000,
20
+ cacheReadTokens: 2000,
21
+ cacheWriteTokens: 0,
22
+ };
23
+ /** The fit as the package takes it: a coefficient with its range. */
24
+ const coeff = { kIn: r(FIT.k_in.value), kCache: r(FIT.k_cache.value) };
25
+ describe('the prefill term', () => {
26
+ it('is the fit times the active parameters and the tokens', () => {
27
+ const without = remoteEnergyBreakdown(USAGE, ARCH, THROUGHPUT);
28
+ const with_ = remoteEnergyBreakdown(USAGE, ARCH, THROUGHPUT, { prefill: coeff });
29
+ const expected = FIT.k_in.value * 30 * 4000 + FIT.k_cache.value * 30 * 2000;
30
+ expect(with_.serverWh[0] - without.serverWh[0]).toBeCloseTo(expected, 12);
31
+ expect(with_.serverWh[1] - without.serverWh[1]).toBeCloseTo(expected, 12);
32
+ });
33
+ it('reaches the impact through the data centre overhead', () => {
34
+ const without = estimateRemote(USAGE, ARCH, SITE, THROUGHPUT);
35
+ const with_ = estimateRemote(USAGE, ARCH, SITE, THROUGHPUT, { prefill: coeff });
36
+ const prefillWh = FIT.k_in.value * 30 * 4000 + FIT.k_cache.value * 30 * 2000;
37
+ expect(with_.energyWh[0] - without.energyWh[0]).toBeCloseTo(prefillWh * SITE.pue[0], 12);
38
+ expect(with_.notes).toContain('prefill term on (OpenAgentsy extension, not EcoLogits)');
39
+ });
40
+ it('propagates the range of the coefficient', () => {
41
+ const low = remoteEnergyBreakdown(USAGE, ARCH, THROUGHPUT, {
42
+ prefill: { kIn: r(FIT.k_in.range[0]), kCache: r(FIT.k_cache.range[0]) },
43
+ });
44
+ const high = remoteEnergyBreakdown(USAGE, ARCH, THROUGHPUT, {
45
+ prefill: { kIn: r(FIT.k_in.range[1]), kCache: r(FIT.k_cache.range[1]) },
46
+ });
47
+ expect(low.serverWh[0]).toBeLessThan(high.serverWh[0]);
48
+ expect(low.serverWh[1]).toBeLessThan(high.serverWh[1]);
49
+ });
50
+ it('changes nothing when it is not asked for', () => {
51
+ const usage = { ...USAGE, outputTokens: 150_000 };
52
+ const impact = estimateRemote(usage, ARCH, SITE, THROUGHPUT);
53
+ const withoutTokens = estimateRemote({ ...usage, inputTokens: 0, cacheReadTokens: 0 }, ARCH, SITE, THROUGHPUT);
54
+ // The 2a behaviour, unchanged: the input tokens do not count, and the
55
+ // estimate of the plan's worked example still holds.
56
+ expect(impact.energyWh).toEqual(withoutTokens.energyWh);
57
+ expect(Math.abs(impact.energyWh[0] - 197.2) / 197.2).toBeLessThan(1e-3);
58
+ expect(impact.notes).not.toContain('prefill term on (OpenAgentsy extension, not EcoLogits)');
59
+ });
60
+ it('pins the fitted numbers, so a refit cannot drift silently', () => {
61
+ expect(FIT.k_in.value).toBeCloseTo(7.187297432523553e-7, 18);
62
+ expect(FIT.k_cache.value).toBeCloseTo(FIT.k_in.value * 0.1, 18);
63
+ expect(FIT.k_in.range[0]).toBeLessThan(FIT.k_in.value);
64
+ expect(FIT.k_in.value).toBeLessThan(FIT.k_in.range[1]);
65
+ expect(FIT.diagnostics.residuals.length).toBe(12);
66
+ expect(FIT.data_source.measurements).toBe(12);
67
+ });
68
+ it('names the source of every number', () => {
69
+ expect(FIT.k_in.source).toContain('2505.06371');
70
+ expect(FIT.k_in.fittedOn).toMatch(/^\d{4}-\d{2}-\d{2}$/);
71
+ expect(FIT.k_cache.source).toContain('assumption');
72
+ expect(FIT.method.length).toBeGreaterThan(3);
73
+ expect(FIT.notes.join(' ')).toContain('does not ship enabled');
74
+ });
75
+ });
76
+ //# sourceMappingURL=prefill.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefill.test.js","sourceRoot":"","sources":["../src/prefill.test.ts"],"names":[],"mappings":"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"}