vec_log per-agent population mean - average over agents instead of over completed episodes - #459
vec_log per-agent population mean - average over agents instead of over completed episodes#459eugenevinitsky wants to merge 13 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR changes how Drive environment metrics are aggregated in vec_log to remove completion-rate bias by switching from “mean over completed episodes” to a two-stage “per-agent window mean, then population mean across agent slots” approach. It also adds a Drive-specific vec_prepare_log step to drain per-agent accumulators into env->log immediately before vec_log.
Changes:
- Add per-agent log accumulators in Drive and a
prepare_log()drain step to produce per-agent population means. - Add a new C binding
vec_prepare_logand call it fromdrive.pybeforevec_logat eachreport_interval. - Relax
vec_log’s emission gate inenv_binding.hfromaggregate.n < num_agentstoaggregate.n < 1(API arg retained but unused).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| pufferlib/ocean/env_binding.h | Changes vec_log emission gating to emit with any data (ignores num_agents arg). |
| pufferlib/ocean/drive/drive.py | Calls new binding.vec_prepare_log() prior to binding.vec_log(). |
| pufferlib/ocean/drive/drive.h | Adds per-agent log sum/count buffers; introduces ensure_per_agent_log_capacity() and prepare_log(); rewrites add_log() to accumulate per-agent. |
| pufferlib/ocean/drive/binding.c | Exposes vec_prepare_log Python binding to call prepare_log() across VecEnv envs. |
Comments suppressed due to low confidence (1)
pufferlib/ocean/drive/binding.c:55
- vec_prepare_log_py doesn’t validate its argument count. Most other vec_* entrypoints in env_binding.h explicitly check PyTuple_Size and raise a clear TypeError; without this, accidental extra args will be silently ignored and missing args will lead to confusing errors inside unpack_vecenv.
prepare_log((Drive *) vec->envs[i]);
}
Py_RETURN_NONE;
}
static int my_put(Env *env, PyObject *args, PyObject *kwargs) {
PyObject *obs = PyDict_GetItemString(kwargs, "observations");
if (!PyObject_TypeCheck(obs, &PyArray_Type)) {
PyErr_SetString(PyExc_TypeError, "Observations must be a NumPy array");
return 1;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| static void ensure_per_agent_log_capacity(Drive *env, int needed) { | ||
| if (env->per_agent_log_capacity >= needed) { | ||
| return; | ||
| } | ||
| int old_cap = env->per_agent_log_capacity; | ||
| env->per_agent_log_sum = (Log *) realloc(env->per_agent_log_sum, needed * sizeof(Log)); | ||
| env->per_agent_log_count = (int *) realloc(env->per_agent_log_count, needed * sizeof(int)); | ||
| memset(&env->per_agent_log_sum[old_cap], 0, (needed - old_cap) * sizeof(Log)); | ||
| memset(&env->per_agent_log_count[old_cap], 0, (needed - old_cap) * sizeof(int)); | ||
| env->per_agent_log_capacity = needed; | ||
| } |
| // Emit whenever any env has data. With Drive's per-agent prepare_log | ||
| // path, aggregate.n is the cross-env count of agents that contributed | ||
| // a window-mean (not completed-episode count), so the meaningful gate | ||
| // is "at least one contribution." Other ocean envs that don't run | ||
| // prepare_log retain completed-episode-count semantics and now emit | ||
| // smaller batches more often — the Python-side mean_and_log | ||
| // (pufferl.py) re-averages across emissions in its rate-limit window. | ||
| if (aggregate.n < 1) { | ||
| return dict; |
vcharraut
left a comment
There was a problem hiding this comment.
Do you have an example of the same seed with and without this PR ?
|
great question, will launch two runs and shre |
|
@copilot resolve the merge conflicts in this pull request |
Conflicts are resolved in commit |
|
@eugenevinitsky The smoke train test didn't go through, I guess the test itself might require a modification to take your changes into account |
63093b1 to
8b543a8
Compare
|
Rebased as a clean re-application onto current 3.0 (the old branch predated the goal-system rewrite #512, the metrics refactor, and the Hydra config switch — a mechanical rebase conflicted on every hunk, so the branch was force-pushed with the change rebuilt on top of Differences from the original:
Local verification: 84/84 unit tests (including the three behavioral logging tests) and the full C suite pass. |
Rebased onto current 3.0 (the original branch predated the goal-system rewrite and metrics refactor). add_log no longer sums completed episodes into env->log — that weighted agents by completion frequency, so short-episode agents (early crashes) dominated the reported means and curves only looked clean after a resample-forced synchronized reset. Each agent slot now keeps an EMA of its completed-episode Log (seeded on first completion, then slot = alpha*slot + (1-alpha)*episode); prepare_log sums the seeded slots into env->log on demand without resetting them, and vec_log's divide-by-n yields a population mean with one weight per agent. binding.vec_prepare_log rebuilds the slots right before each vec_log call, inside drive.py's existing not-eval_mode guard (prepare_log overwrites env->log, which eval reads per episode). The vec_log gate becomes n >= 1 (the old n >= num_agents throttle made no sense for persistent per-agent state). log_ema_alpha is exposed end-to-end (drive.h, binding, drive.py, config_schema, puffer_drive.yaml; default 0.707 ~ 2-episode half-life). expert_static_car_count and static_car_count move into the per-agent episode log, so the divide-by-n recovers the true per-env count rather than count/num_agents. Their logged scale changes accordingly. Behavioral tests cover the emission gate, steady-state n == num_agents, and EMA persistence across emissions without new completions. Smoke goldens are not included; they are only bit-reproducible in the pinned CI image and are regenerated there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
d3f7ac7 to
e908109
Compare
add_log wrote only the per-agent EMA slots, leaving env->log populated solely by prepare_log via the Python vec_prepare_log binding. Consumers that drive the env directly in C never reach that path, so tests/drive/test_drive_env_smoke.c saw env.log.n == 0 and failed both its saw_log and truncation assertions. Calling prepare_log at the end of add_log keeps env->log valid for any C-side reader. drive.py's per-report vec_prepare_log call stays: vec_log zeroes env->log after each emission, so envs with no completion since the last emission must still be rebuilt from their slots to contribute to the cross-agent mean.
…golden] The per-agent EMA turned every Log field into a gauge, but total_distance_travelled and total_infractions feed total_distance_travelled_sum / total_infraction_count, which reduce_environment_metrics and the eval report sum across emissions to build avg_distance_per_infraction from global totals. Summing a gauge double-counted them once per emission rather than once per completed episode. Both fields now accumulate raw per-episode values directly into env->log and are excluded from the EMA slots; prepare_log carries them across its memset so only vec_log's post-emission zeroing clears them. That restores reset-on-consumption, so emissions with no completed episode contribute zero instead of re-reporting the running total. Measured over 60 steps at scenario_length=10: 51 emissions, of which 6 carry data, matching the 6 episode ends. The summed distance drops from 3590.4 to 422.4.
binding.c and drive.h conflicted only where 3.0 added code adjacent to this branch's: the seed-plumbing helpers (unpack_seed, my_reseed) landed where vec_prepare_log_py sits, and log_episode_seed landed where add_log calls prepare_log. Both sides kept. Smoke goldens reset to 3.0's; they are regenerated in CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…golden] T1-T3 run every agent in lockstep, which is the one regime where completion-weighted and agent-weighted aggregation agree: with the base implementation spliced back in, two of the three still passed. T4 gives sub-envs independent truncation (termination_mode=1, infractions remove) so completions arrive staggered, then asserts the emitted population climbs to num_agents and stays there. It fails on 3.0 and passes here. log_ema_alpha reaches the EMA update straight from config with no range check. At 1.0 the (1 - alpha) term vanishes and every metric stays pinned to its first completed episode for the whole run; above 1.0 the slots diverge. Both are silent, so reject them at construction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Its assertion — n == num_agents at the last emission with agents in lockstep — is a strict subset of what the staggered test already asserts, and it passes against the completion-sum implementation, so it guards nothing the other two don't. The gate test stays: it is the only one that pins the no-emission-before-any-completion behavior. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
avg_distance_per_infraction pools every episode completed in the interval, so it answers "how far does the fleet travel per infraction" — dominated by whichever agents drive the most. That is the right question for fleet safety, but it is the completion-weighted scheme the rest of this PR moves away from, so on its own it no longer matches the other curves. Add the same ratio under per-agent weighting. agent_weighted_distance_ travelled and agent_weighted_infractions are ordinary EMA slots, so their ratio counts one episode per agent and answers "how far does a typical agent go between infractions". Both are now reported. The two are the same quantity when agents complete equally often and diverge only when completion rates differ: measured over 400 steps at alpha=0, lockstep gives 30.601 vs 30.621, while sub-envs truncating independently give 1339.776 vs 76.729. T3 pins the lockstep identity. The pooled pair resets on consumption and so must be summed across emissions; the agent-weighted pair persists and must be averaged. The emitted names avoid _sum/_count for the latter to keep that straight. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What
vec_logreported a mean over completed episodes, which weighted each agent by how often it finished. Agents that crashed early finished more often and so dominated everyenvironment/*curve. This changes the aggregation to a mean over agent slots: one weight per agent, regardless of completion frequency.