Skip to main content

Server-measurement architecture

Offline measurement is one engine lifetime that produces one measured window over a fixed prompt batch. Server-mode measurement is one server lifetime that produces many windows, driven by traffic that arrives over time. This page explains the three seams that add server mode without re-keying the offline sweep loop or duplicating the energy-window mechanics: the traffic source, the server session, and the window manager.

For the measurement model these seams implement (windows, warmup gate, the comparability caveats), see Methodology: server-mode measurement.

The problem

The offline session seam already separates a session's lifetime (acquire -> produce -> release) from result production, and an offline session deliberately produces exactly one result per lifetime. Server mode needs the same lifetime discipline but a different result cardinality: one launched server, held alive across a rate sweep, producing one result per measurement window. It also needs a load driver the offline path never had, and it must keep the energy-window mechanics the harness already owns rather than reimplementing them.

Three seams answer this. Each is a narrow interface so the online-serving parts plug in beside the offline parts instead of rewriting them.

The TrafficSource seam

TrafficSource is a typing.Protocol in src/llenergymeasure/harness/traffic.py:247. It has a single method - run(transport, *, drain_timeout=None) -> IssuerReport - and it is the one window-manager-facing surface for driving online load. It is a deliberate plugin point: a load-generator-backed source can arrive later behind the same interface without touching the window manager.

The shipped implementation is OpenLoopPoissonSource (traffic.py:263). It builds a deterministic arrival schedule from (rate, arrival, burstiness, seed) and issues each request on schedule as a detached task, without awaiting the transport call. That is the open-loop guarantee, and it is why a stalled transport cannot inflate the measured issuance duration or stall the schedule: the issue loop only sleeps to the next scheduled offset, and a bounded drain (drain_timeout) cancels whatever is still pending. The transport itself is a companion Transport protocol (traffic.py:84), injected and never owned by the traffic module.

Per-request bookkeeping lives on RequestRecord (traffic.py:96): issued_at (the ideal scheduled time, the latency anchor), dispatched_at (when the request actually left for the transport), and completed_at (when it reached a terminal state). The gap between issued and dispatched is what a concurrency cap or a slow transport shows up as - it is recorded, not hidden.

The ServerSession

ServerSession (src/llenergymeasure/study/server_session.py:642) is the one-dispatch, N-results session type - the sibling the offline session seam was built to admit. It duck-types the same ExperimentSession protocol the offline SubprocessSession and DockerSession implement, but its single lifetime produces many results:

  • __enter__ (server_session.py:742) launches the engine server and waits for readiness (the launch and the model load ride inside one instrumented phase). A failure during acquisition tears down whatever was acquired and re-raises, so a failed launch never leaks.
  • run() (server_session.py:790) warms the server before each rate level and drives the window manager, producing one per-window result for every window of every level.
  • __exit__ (server_session.py:835) drains and finalises the bundles, then tears the server down. It is idempotent and swallows cleanup faults with a loud warning, so a fault during teardown never converts a completed measurement into a failure.

Session grouping

A rate sweep does not have to relaunch the server per rate. partition_server_groups (server_session.py:1935) folds a run of consecutive server cells that are identical except server.traffic.rate and belong to the same cycle into one session - one launch driving each rate as its own level. A non-server cell, any non-rate difference, or a cycle-number change ends the group. This is why experiment order changes launch economics: under interleave a sweep pass folds into one launch, while under the default sequential order with more than one cycle each cell's cycles are adjacent, so the sweep dispatches singleton sessions (one launch per cell per cycle).

Each window's identity is minted by _window_experiment_id (server_session.py:1506) as server-{config_hash}-c{cycle}-L{level_index}-W{window_index}. The cycle component keeps cycle 1 and cycle 2 of one grid point distinct so their bundles do not collide once a reader keys on experiment_id.

The window manager

The window manager owns the per-window mechanics. A window is described by a WindowSpec (src/llenergymeasure/harness/window_manager.py:125) - rate, duration, ramp exclusion, and attribution policy. For each level the manager excludes the ramp once and prospectively, then opens and closes a fixed number of contiguous windows, bracketing each with a fresh measurement bracket so the energy-window mechanics are reused unchanged from the offline path.

Validity is decided per level, not per window, by validate_level_stability (window_manager.py:617). Each window's energy per token is its integrated GPU energy over the tokens attributed to the span; a level passes only when the coefficient of variation of energy per token across the consecutive windows is stable through the end of the level (below the same auto threshold the offline steady-state detector uses). A window with zero attributed tokens makes the diagnostic unformable and fails the level. A failing level is stamped invalid with a reason, never silently dropped.

Interrupt semantics

On SIGINT mid-session the interrupt watcher cancels the task driving the current level, and the session preserves whatever level state exists before re-raising so __exit__ reaps the server. Crediting is conservative (server_session.py:1244): only cleanly-closed levels - those whose validation resolved and whose bundles are finalised - are credited as completed, so a resume does not re-run them. In-flight, aborted, and unreached cells stay marked running for the sweep loop's interrupt downgrade; their work is not credited. If a grouped session is fully invalid (a warmup abort, or no valid window), it is counted as its full cell count of failures rather than one.

The server lifecycle underneath

The session composes the container and process primitives in src/llenergymeasure/infra/server_lifecycle.py. Launch allocates a free loopback port and runs the image detached; readiness (await_ready, server_lifecycle.py:417) runs a liveness health poll and then a real inference request through the serving path, and is satisfied only when that request returns HTTP 200; shutdown (server_lifecycle.py:630) stops and force-removes the container so nothing leaks even if the stop failed.

Why this matters

The offline and server paths share the harness's energy-window mechanics and the study runner's session seam. Adding online-serving measurement did not mean a second measurement stack: it meant one load-driver protocol, one N-results session beside the one-result offline sessions, and a window manager that reuses the same measurement bracket. A load-generator-backed traffic source can later replace OpenLoopPoissonSource behind the TrafficSource protocol without any of the session or window logic changing.