Skip to content

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

Draft
davidberenstein1957 wants to merge 1 commit into
feat/add-fastapi-middlewarefrom
feat/fastapi-measurement-tiers
Draft

feat(fastapi): measurement capability tiers for per-request energy#1381
davidberenstein1957 wants to merge 1 commit 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

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>
@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.25843% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.08%. Comparing base (5680063) to head (64b0622).

Files with missing lines Patch % Lines
codecarbon/integrations/fastapi/tiers.py 93.33% 6 Missing ⚠️
codecarbon/integrations/fastapi/middleware.py 93.75% 5 Missing ⚠️
codecarbon/emissions_tracker.py 85.71% 1 Missing ⚠️
Additional details and impacted files
@@                       Coverage Diff                       @@
##           feat/add-fastapi-middleware    #1381      +/-   ##
===============================================================
+ Coverage                        92.06%   92.08%   +0.02%     
===============================================================
  Files                               53       54       +1     
  Lines                             5530     5700     +170     
===============================================================
+ Hits                              5091     5249     +158     
- Misses                             439      451      +12     

☔ 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant