Skip to content

feat(fastapi): measurement capability tiers for per-request energy - #1381

Merged
davidberenstein1957 merged 8 commits into
feat/add-fastapi-middlewarefrom
feat/fastapi-measurement-tiers
Aug 19, 2026
Merged

feat(fastapi): measurement capability tiers for per-request energy#1381
davidberenstein1957 merged 8 commits into
feat/add-fastapi-middlewarefrom
feat/fastapi-measurement-tiers

Conversation

@davidberenstein1957

Copy link
Copy Markdown
Collaborator

Targets feat/add-fastapi-middleware (PR #1203), not master.

Problem

The middleware reported a per-request energy figure regardless of whether the
backend could resolve a request. Measured: 27 of 30 short requests reported
exactly 0.0. Zero is a factual claim that the request was free.

Tiers

Resolved at startup from the hardware the tracker actually detected
(tracker._hardware), weakest component wins (RAM is analytic and does not vote).

Tier Backends Per-request energy
measured Linux RAPL; NVML only when aggregating >= 1 s reported
estimated constant/TDP, RAM reported, labelled: analytic in duration
aggregate_only cpu_load, macOS powermetrics, amdsmi, Windows EMI, intel_power_gadget not reportedendpoint_totals() only

Measurements behind this:

  • Linux RAPL — counter updates ~1 ms, quantum 15.3 uJ, read cost 10.4 us (0.38 us holding the fd with os.pread). Genuinely resolves a 30 ms request.
  • NVML — sensor period 20 ms (V100) to ~100 ms with a 25 ms averaging window (A100/H100); 75-80% of elapsed time unsampled. Sub-100 ms GPU energy is not obtainable.
  • macOS powermetricsget_details() spawns sudo powermetrics -n 10 -i 100, blocks ~1 s per read. Cannot be in a request path.
  • constant/TDPpower = tdp * 0.5, so energy is exact at any resolution but only restates wall time.
  • cpu_loadpsutil.cpu_percent(interval=None) is tick-quantized; 98% of 5 ms reads return zero load, and 0.1 + 0.9*(load/100)**3 turns that into a 10%-of-TDP floor. Verified: 30 requests on a core pegged at 100% all reported exactly 0.0900 J.
  • amdsmi / Windows EMI — update rate undocumented; treated as unprobed.

API

  • MeasurementTier enum, detect_measurement_tier(hardware, window_seconds) -> TierDetection(tier, components).
  • RequestMeasurement on request.state.codecarbon: tier, available, emissions, energy_consumed, unavailable_reason, emissions_data (None when not reportable). on_request_complete keeps its signature and receives None for emissions_data when the tier cannot report.
  • middleware.measurement_tier, middleware.tier_detection, middleware.endpoint_totals() (valid in every tier).
  • Response headers stay off by default; when opted in they carry X-CodeCarbon-Tier alongside the number, and the number reads unavailable rather than 0.
  • Idle/baseline power is charged to requests, not subtracted (keeps per-endpoint totals summing to the run total). Documented in docs/how-to/fastapi.md.

Unavailable, not zero

A request that spans no completed sampling window yields
available=False / unavailable_reason="request spanned no completed sampling window".
In the ESTIMATED tier a zero sampled delta is instead filled analytically
(P x elapsed with the tracker's implied kg/kWh); if intensity is not yet known
it reports unavailable rather than 0.

Also in here

  • Bounded _tasks: EmissionsTracker.discard_task() evicts a finished task; the middleware calls it after persist_completed_task. Test drives 10,000 request cycles through the real tracker methods and asserts the map stays at 0.
  • Bug fix: finish_http_request set duration to the request duration and then subtracted the baseline duration, producing negative per-request durations (observed -29 s end to end). Now passes absolute elapsed so the delta is the request duration and emissions_rate is computed against it.

Test evidence

  • tests/integrations/test_fastapi_tiers.py (new, 24 tests): every test injects known hardware stubs and known EmissionsData values — nothing depends on the test machine (see PR feat(cli): live local dashboard for monitor #1365: a < 90 W assertion that passed on Apple Silicon and failed on Linux CI at 280 W).
  • tests/integrations/test_fastapi_middleware.py: mock trackers now inject _hardware=[intel_rapl] explicitly instead of inheriting whatever the runner has.
  • uv run pytest tests/ -q --ignore=tests/test_viz_data.py: 709 passed, 21 skipped.
  • uv run pre-commit run --all-files: all hooks pass.
  • examples/fastapi_middleware.py run end to end on Apple Silicon (aggregate_only):
    • X-CodeCarbon-Tier: aggregate_only, X-CodeCarbon-Emissions-kg: unavailable
    • /totals -> {"tier":"aggregate_only","endpoints":{"GET /predict":{"count":4,"duration":3.05,"energy_consumed":3.69e-05,"emissions":6.46e-06}}}
    • i.e. honest unavailability per request, real numbers in aggregate.

Deliberately deferred

  • Concurrency / per-window attribution — a separate design prototype is in flight. RequestMeasurement and EndpointTotals are per-endpoint aggregates, so per-window attribution can be added under _build_measurement / _record_totals without reshaping the API.
  • NVML aggregate pathdetect_measurement_tier takes window_seconds and returns MEASURED for NVML at >= 1 s, but the middleware only ever calls it with a per-request window (0). Wiring a real aggregation window is a follow-up.
  • Tier on EmissionsData / CSV / API schema — kept out of the shared output schema; the tier lives in the FastAPI-side result object.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.15584% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.06%. Comparing base (761d374) to head (7e363ba).

Files with missing lines Patch % Lines
codecarbon/integrations/fastapi/tiers.py 92.50% 6 Missing ⚠️
codecarbon/integrations/fastapi/middleware.py 95.83% 3 Missing ⚠️
Additional details and impacted files
@@                       Coverage Diff                       @@
##           feat/add-fastapi-middleware    #1381      +/-   ##
===============================================================
+ Coverage                        92.01%   92.06%   +0.04%     
===============================================================
  Files                               53       54       +1     
  Lines                             5451     5598     +147     
===============================================================
+ Hits                              5016     5154     +138     
- Misses                             435      444       +9     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@davidberenstein1957
davidberenstein1957 marked this pull request as ready for review August 13, 2026 05:16
@davidberenstein1957
davidberenstein1957 requested a review from a team as a code owner August 13, 2026 05:16
davidberenstein1957 and others added 8 commits August 19, 2026 11:43
Report per-request energy only when the detected backend can resolve a
request; otherwise report unavailable (never 0.0) and expose per-endpoint
totals instead.

- MEASURED: Linux RAPL (1 ms counter, 15.3 uJ quantum); NVML only >= 1 s
- ESTIMATED: constant/TDP + RAM (analytic in duration)
- AGGREGATE_ONLY: cpu_load, powermetrics, amdsmi/EMI (unprobed)

Tier is part of the output (RequestMeasurement on request.state.codecarbon,
middleware.measurement_tier, X-CodeCarbon-Tier when headers are opted in),
not a log line. Detection is duck-typed so tests inject known hardware.

Also: bound tracker._tasks via discard_task() after persistence (one Task
per request leaked for the process lifetime), and fix the negative
per-request duration in finish_http_request.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The middleware base replaced _TrackerRunner with a ThreadPoolExecutor and
now resolves the task label inside finish_http_request, so the tier layer
folds into that flow instead of carrying its own runner and stop_task
fallback. The duplicated finish_http_request duration fix and discard_task
are already in the base and are dropped here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
detect_measurement_tier was always called without a window, so it defaulted
to 0.0 and every NVML machine fell to AGGREGATE_ONLY with no per-request
emissions at all. Pass the tracker's measure_power_secs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n "RAM"

The voting filter tested the repr string, so renaming the RAM class would
have silently made it vote and dragged every detection down a tier.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tracker scales _total_energy by PUE, so the P x hours estimate has to
scale too or endpoint_totals() stops summing to the run total on any
pue > 1 configuration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nsity

Three defaulted getattrs and a try/except for exceptions a float division
of two floats cannot raise, replaced by one guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ier entries

available/emissions/energy_consumed each restated "emissions_data is None",
and three CPU_MODE_TIERS entries mapped to the value .get() already returns
as its default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@davidberenstein1957
davidberenstein1957 force-pushed the feat/fastapi-measurement-tiers branch from c94fe6c to 7e363ba Compare August 19, 2026 09:50
@davidberenstein1957
davidberenstein1957 merged commit 3fa148f into feat/add-fastapi-middleware Aug 19, 2026
7 checks passed
@davidberenstein1957
davidberenstein1957 deleted the feat/fastapi-measurement-tiers branch August 19, 2026 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant