Add always-on critical-path timing for trainers - #58
Closed
micahtyong wants to merge 4 commits into
Closed
micahtyong wants to merge 4 commits into
micahtyong wants to merge 4 commits into
Conversation
Contributor
|
I'll fix CI failures and address comments from users with write access that start with 'DevinAI' or '@devin'.
|
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration
Bot
force-pushed
the
devin/1790102631-critical-path-instrumentation
branch
from
September 22, 2026 18:44
628708a to
236e948
Compare
5 tasks
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
CriticalPath now implements Observer and derives queue_wait and phase durations from the begin/register_model/span callbacks the engine already emits, so the engine carries no timing code of its own. TrainerTelemetry and CriticalPath are composed with an Observers fan-out in the Modal serve path. Also: evict a model's series on forget_model so long-lived trainers do not exhaust MAX_SERIES, stop double-counting when model_id is the aggregate key, and only let aggregate polls suppress the periodic log line. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Contributor
Author
|
/devin review |
Contributor
log_critical_path now runs the fetch and log on a single background worker (extra calls while one is in flight are dropped), captures the active W&B run in the caller, and swallows W&B failures alongside fetch failures so a logging error can never abort a step after reset=true. Values log with commit=False against a lilo/step axis so late reports never collide with the run's global step; flush_critical_path() waits for the last report before wandb.finish(). Also: Engine composes a caller-supplied observer with timings instead of replacing it, and aggregate series are exempt from MAX_SERIES so the trainer-wide totals survive many model_ids. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A client step time today is one number that mixes "my work took this long on the GPU" with "the trainer was busy with somebody else's LoRA". Nothing separates those unless OTLP is configured against a tracing backend (off by default) — which is why "is this Lilo overhead or fair sharing?" keeps being unanswerable after the fact.
This adds an exporter-free breakdown the trainer always keeps, readable per model, so a training loop can log it to W&B per step (or stdout when there is no W&B).
One instrumentation path, not three
CriticalPath(lilo/telemetry/critical_path.py) is an implementation of the engine's existingObserverprotocol — the same callbacks that feedTrainerTelemetry's OTLP spans. The engine carries no timing code of its own: nosubmitted_atfields, notimings.record(...)call sites.The only engine change is passing
request_ids=on the execute span (needed because a coalesced batch can hold several commands for one model, soseq_idsdoesn't zip withmodels), plusObservers(*observers), a fan-out so the Modal serve path runs both.Engine(observer=x)composesxwith the timing observer rather than replacing it, so timing can't be turned off by accident:CriticalPathis a bounded in-memory accumulator:(model_id, phase) -> {count, total_s, mean_s, max_s, mean_batch}, plus single-valued startup gauges. Every record credits both the model's series and anALL_MODELSaggregate.executeis recorded on the failure path too.mean_batchis the number of commands coalesced into one backend execution — the multi-LoRA batching factor.Startup is three gauges measured from process start, set once:
trainer.backend_ready_s(Megatron answering/healthz),trainer.serving_ready_s,trainer.first_model_ready_s.Reading it
GET /api/v1/timing?model_id=...&reset=trueon the engine, proxied by the control plane throughengine_for(model_id), so a client hits its own trainer.lilo.timing.log_critical_path(model_id, step=step)— flattens the snapshot tolilo/<phase>.<stat>scalars, logs to the active W&B run if one exists (discovered viasys.modules, so W&B stays a non-dependency), prints a JSON line otherwise, and swallows every exception (fetch andrun.log) so instrumentation can't fail a training step.reset=Trueby default, making each call the interval since the last.lilo_critical_pathJSON line every 5 min, so container logs alone explain a step.Off the training thread
log_critical_pathreturns immediately; the HTTP fetch and the log run on one daemon worker. Correctness guards, since a late-arriving log is the classic way to corrupt a W&B run:_Reporter.submit), so a slow control plane costs at most one outstanding request and can't pile up threads. The next successful call covers both intervals because counters weren't reset in between.step=values behind the run's current step, which a background thread can't avoid. Values go out withcommit=Falseand alilo/stepcolumn, withdefine_metric("lilo/*", step_metric="lilo/step")(once per run) so charts plot against the step they describe:flush_critical_path(timeout)joins the in-flight report; call it beforewandb.finish().background=Falsegives the old blocking behavior and returns the metrics.Unchanged and worth knowing:
reset=trueis destructive, so two readers of the same model (e.g. a background fetch plus a manualcurl) split the interval between them.Semantics worth knowing
queue_waitis submit → execution start. A pipelined client's own backlog counts toward it, not just neighbors.executefor a coalesced batch is the batch's wall time, credited to every model in it.forget_model), so a long-lived multi-tenant trainer never exhaustsMAX_SERIES; the aggregate keeps them. The*aggregate is exempt from the cap (it's bounded by the phase count), so per-model churn can never drop trainer-wide totals.Nothing here is configurable or optional: no env var, no exporter, no payload capture, bounded at
MAX_SERIESper-model entries.Link to Devin session: https://modal.devinenterprise.com/sessions/2ea9fb5c53e5482c86a85dffa85b2c7c
Open in Devin Desktop: https://modal.devinenterprise.com/desktop/session/2ea9fb5c53e5482c86a85dffa85b2c7c?variant=devin
Requested by: @micahtyong