Skip to content

Add chunk_memory_mode: auto — container-memory-aware adaptive chunking - #1104

Open
vincentgong7 wants to merge 1 commit into
ActivitySim:mainfrom
vincentgong7:feature/auto-chunk-memory
Open

Add chunk_memory_mode: auto — container-memory-aware adaptive chunking#1104
vincentgong7 wants to merge 1 commit into
ActivitySim:mainfrom
vincentgong7:feature/auto-chunk-memory

Conversation

@vincentgong7

Copy link
Copy Markdown
Contributor

Summary

The current adaptive chunking mechanism uses a manually tuned, static chunk_size and effectively
assumes that host RAM is fully available. This makes the chunk size easy to misconfigure, particularly
in memory-constrained environments such as Kubernetes pods, where the process may unknowingly exceed its
cgroup memory limit and be terminated by the OOM killer.

To address this, we introduce an opt-in chunk_memory_mode: auto. When enabled, adaptive chunking
determines its chunk size at runtime based on the process's actual memory limit, eliminating the need
for per-machine tuning and reducing the risk of container OOM failures.

The change is fully backward compatible: the default fixed mode preserves the existing behavior.

Motivation

How adaptive chunking currently works. For each submodel, ActivitySim divides choosers into batches
("chunks") sized to fit within memory. The chunk size is determined using the user-configured
chunk_size, which represents the approximate amount of RAM available for batch processing.

In training mode, ActivitySim measures the actual memory usage of each submodel and caches an estimated
per-row memory footprint. The adaptive and production modes then reuse and refine these cached
estimates to determine the appropriate number of rows per chunk.

The problems with that.

  1. chunk_size is a static number the user must hand-tune to the machine (RAM, cores, #households,
    skim size) and re-tune for every new machine or model — the docs themselves describe a trial-and-error
    procedure to find it.
  2. It effectively targets host RAM. Inside a container (k8s / cgroup) the process is OOM-killed at the
    cgroup limit, which is usually well below host RAM. A chunk_size set from host RAM — or the common
    chunk_size: 0 / "use most of the RAM" guidance — over-commits and the run is OOM-killed, often deep
    into a long multiprocess run.
  3. Worker count is a separate manual knob. num_processes must also be hand-tuned against the same
    RAM; too many workers for the available memory OOMs, and the right number changes per machine.

How this change solves them. chunk_memory_mode: auto derives the chunk budget from the process's
real memory ceiling (the cgroup limit that actually OOM-kills it) at runtime, divides it correctly
across the real worker count (and can auto-pick that count), and hardens the sizing so a large shared
memory-mapped skim buffer can't distort it. The result needs no per-machine chunk_size tuning and
does not exceed the container's memory limit.

What it does (when chunk_memory_mode: auto)

  • Budget from the real ceiling: (memory_limit − current usage) × chunk_memory_safety_factor,
    where memory_limit comes from the cgroup (v2 memory.max → v1 memory.limit_in_bytespsutil
    host RAM). No machine-specific chunk_size to set.
  • Multiprocess-aware: the shared budget is divided by the real per-step worker count; set
    num_processes: 0 to auto-derive the worker count from available (non-reclaimable) memory and a
    chunk_worker_target_budget per-worker target.
  • Shared-memory-robust sizing: chunks are sized from the budget against each chunk's incremental
    memory growth, so a large memory-mapped shared skim buffer (reclaimable page cache) does not distort
    the row-size estimate and collapse chunks to a single row.
  • Guards: a budget floor keeps chunking active under pressure (a zero budget would disable chunking
    and process all choosers at once); a growth cap and an incremental peak-backoff bound overshoot; an
    optional circuit-breaker warns as memory nears the limit.

It reuses the existing adaptive-chunking machinery (ChunkSizer / observed-memory / row-size cache) —
an enhancement of adaptive chunking, not a replacement.

New settings (all under auto; defaults preserve current behavior)

setting default meaning
chunk_memory_mode fixed auto derives the budget from the real ceiling; fixed = static chunk_size
chunk_memory_safety_factor 0.75 fraction of the available ceiling used as the budget
chunk_worker_target_budget 0 per-worker budget (bytes) for auto worker count when num_processes: 0
chunk_growth_cap 0 max multiplicative growth of rows-per-chunk between chunks (0 = off)
chunk_row_size_margin 1.0 safety multiplier on the estimated per-row memory
chunk_memory_circuit_breaker false warn as memory approaches the ceiling
chunk_memory_abort_ratio 0.9 fraction of the ceiling at which back-off / warning triggers

Backward compatibility

All new behavior is gated on chunk_memory_mode. The default fixed returns the static chunk_size
verbatim (asserted by a test) — existing runs are unaffected.

Testing

  • New unit tests: activitysim/core/test/test_mem.py (cgroup limit / available / shmem parsing, worker-count
    recommendation) and test_chunk_robust.py (fixed = legacy; auto budget bounds + safety scaling;
    auto-mode simple_simulate output matches fixed-mode). 15 tests pass.
  • Validated end-to-end on a full-sample multiprocess run (~1.2M households, ~8M trips) inside a 62 GiB
    container that the equivalent fixed-chunk_size config could not fit — auto-mode completed with no OOM.

Docs

docs/core.rst — new "Automatic memory-aware chunking (chunk_memory_mode: auto)" subsection.

@vincentgong7
vincentgong7 force-pushed the feature/auto-chunk-memory branch from 1f1f5c9 to f799b82 Compare August 11, 2026 22:27
Adaptive chunking sizes chunks against a static `chunk_size` byte budget that the user
must hand-tune per machine, and it targets host RAM rather than the container's cgroup
limit — so on memory-limited containers it can over-commit and get OOM-killed. This adds
an opt-in `chunk_memory_mode: auto` that derives the chunk memory budget from the process's
real memory ceiling at runtime and hardens the adaptive sizing.

When `chunk_memory_mode: auto`:
- Budget = (memory_limit - current usage) * chunk_memory_safety_factor, where memory_limit
  is read from the cgroup (v2 memory.max, then v1, then psutil) — the limit that actually
  OOM-kills the process. No machine-specific chunk_size to tune.
- Multiprocess: the shared budget is divided by the real per-step worker count (read from
  the num_processes injectable); num_processes:0 auto-derives the worker count from the
  available non-reclaimable memory / chunk_worker_target_budget.
- Chunks are sized from the budget vs the chunk's INCREMENTAL memory (not absolute rss), so a
  large memory-mapped shared skim buffer (reclaimable page cache) does not pollute the
  measurement and collapse chunks to a single row.
- A budget floor keeps chunking active under pressure (a zero budget would disable chunking
  and process all choosers at once); a growth cap and an incremental peak-backoff bound
  overshoot; an optional circuit-breaker warns as memory nears the limit.

All new behavior is gated on `chunk_memory_mode` (default `fixed` = current behavior), so
existing runs are unaffected. Adds unit tests (core/test/test_mem.py, test_chunk_robust.py).
@vincentgong7
vincentgong7 force-pushed the feature/auto-chunk-memory branch from f799b82 to 77df117 Compare August 11, 2026 22:40
@jpn--

jpn-- commented Aug 11, 2026

Copy link
Copy Markdown
Member

Thanks for your proposed contribution. I'm sure you already see the test failures. Based on the content and format of your contribution, I strongly suspect you are using AI for this (if not -- kudos to you on getting this far). There is no restriction on doing so for this project, and indeed I asked for an AI review, which found at least the following issues:

  1. It breaks Windows even in fixed mode.
    mem.py unconditionally imports resource, a Unix-only Python module. ActivitySim explicitly supports Windows, Linux, and macOS, so the claimed backward compatibility is false. ActivitySim platform documentation, Python resource documentation.

  2. Auto mode appears unsafe in production mode.
    The “safe start” replaces the cached row-size estimate with zero after the probe (lines 1058–1078). Production mode does not measure a replacement; it reuses that now-zero estimate and consequently doubles the next chunk repeatedly (lines 1116–1158). The peak backoff is also explicitly disabled in production (lines 1176–1180). This defeats the claimed safety mechanism in the mode intended for normal production runs.

  3. Low-memory conditions can produce a dangerously large budget.
    get_available_memory() legitimately returns zero, but zero is treated as “unknown” and replaced with the full memory limit (lines 229–231). Then a hard 1 GB minimum is applied per worker after division (lines 245–251). Eight workers with only 2 GB of aggregate headroom can therefore receive 8 GB of aggregate chunk budget. This is exactly the failure mode the feature claims to prevent.

  4. The “circuit breaker” is not a circuit breaker.
    Configuration documentation says it aborts and retries a chunk (lines 423–431); the implementation explicitly says abort/retry is future work and merely logs a warning (lines 770–794). It is not called at all in production mode (lines 797–816). In multiprocessing it also compares one process’s RSS with a cgroup-wide limit, rather than using cgroup-wide consumption such as memory.current. Linux cgroup documentation.

Is the benefit demonstrated?

No. There are no runtime comparisons, repeated trials, peak-cgroup-memory traces, multiple model configurations, or sensitivity tests across memory limits and worker counts.

The tests are particularly weak for a memory-control feature: the integration test has three choosers, and there are no tests for production sizing, multiprocess aggregate pressure, zero headroom, the per-worker floor, or actual OOM prevention. The PR description also claims tests for shared-memory parsing and worker recommendations that do not appear in the submitted test files. Linux core tests have passed, but the overall CI run was still in progress when I checked.

Recommended scope

Instead of trying to fix this, perhaps try a different approach. Useful, lower-risk features could include:

  • Cross-platform cgroup-limit detection and logging.
  • Startup warnings or fail-fast checks when the configured chunk/worker plan conflicts with the container limit.
  • An observe-only advisor that reports suggested explicit chunk sizes.

In short: cgroup-aware guardrails are useful; the case for memory-maximizing adaptive chunk optimization has not been made here.

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.

2 participants