Skip to content

feat: add opt-in memray allocation tracking (agent, manager, storage-proxy, app-proxy)#12800

Open
Yaminyam wants to merge 8 commits into
mainfrom
feat/agent-memray-option
Open

feat: add opt-in memray allocation tracking (agent, manager, storage-proxy, app-proxy)#12800
Yaminyam wants to merge 8 commits into
mainfrom
feat/agent-memray-option

Conversation

@Yaminyam

@Yaminyam Yaminyam commented Jul 13, 2026

Copy link
Copy Markdown
Member

Summary

Adds an opt-in memray allocation-tracking option across the agent, manager, storage proxy, and app-proxy, backed by a shared MemrayConfig (common/configs/memray.py). Useful for diagnosing memory growth. Mirrors the profiling setup the app-proxy already shipped (memray.Tracker(follow_fork=True)).

Shared config

MemrayConfig fields:

  • enabled (default false)
  • output-destination (default ./memray-output.bin)
  • native-traces (default false) — also capture native C/C++ stacks (memray --native); higher overhead, needs debug symbols to resolve

Per-component

  • agent[memray] section on AgentGlobalConfig; installs memray.Tracker(follow_fork=True, native_traces=...) in main() before the service worker forks. follow_fork=True is required because aiotools.start_server() forks the worker that does the real work. The capture path gets the PID appended (memray refuses to overwrite an existing file).
  • manager — same wiring; memray on BootstrapConfig (read in main() before the workers fork) and on ManagerUnifiedConfig (for the generated sample).
  • storage-proxy — same wiring; [memray] on StorageProxyUnifiedConfig, installed in main() before the worker forks.
  • app-proxy (coordinator + worker) — converge onto the shared config via a new [profiling.memray] table, non-breaking: the old enable_memray / memray_output_destination keys keep working as deprecated aliases, reconciled by a model_validator when the new table is absent (an explicit [profiling.memray] wins). This also adds native-traces support and the PID-suffixed capture path to app-proxy.

Disabled by default everywhere; enabling adds runtime and disk overhead (the capture grows for the process lifetime).

Worker-side capture close (fix)

The forked worker's capture — the only one worth reading, since workers do the real work — used to end up truncated and unparseable (memray reported a 0.0s span out of a 366 MB file):

  • memray's Tracker writes the capture trailer only from __exit__; it has no __del__ and registers no atexit handler.
  • We only closed the tracker in the master's finally, which never runs in a forked child — and aiotools ends children with a bare os._exit(), so atexit would not run either.

The tracker lifecycle now lives in two helpers next to the Pyroscope profiler: start_memray_tracker() for the master and close_memray_tracker_in_worker() called from each component's forked shutdown path. Tracker.__exit__ is not idempotent, so master and worker each close exactly one tracker: their own (the module global is a per-process copy after the fork).

Notes

  • App-proxy uses snake_case TOML keys, so the shared memray table renders its kebab serialization aliases (output-destination, native-traces); both snake and kebab are accepted on load.
  • Regenerating the app-proxy coordinator sample also pulled in previously un-regenerated schema fields (pool_recycle, pool_pre_ping, traefik client_ip_strategy) that were already merged — the sample is a generated artifact, so it is committed in sync.

Verification

  • Agent verified end-to-end on an internal GPU agent node (real workload): RPC/metric ports open, Prometheus scraping /metrics -> 200, follow_fork capturing the worker + kernel-runner forks (hundreds of thousands of allocations), native-traces=true path confirmed via capture metadata. Rolled back after testing.
  • Worker-side close verified on the same node: after the fix, the worker's PID-suffixed capture parses cleanly with a full time span (previously truncated at the first bad record).
  • App-proxy back-compat reconcile covered by a 4-case check (legacy flat keys / new nested table / absent / both-set precedence).
  • pants fmt/lint/check green across all changed files; component samples regenerated from schema.

🤖 Generated with Claude Code

Add a `[memray]` config section to the agent so its memory allocations
can be captured for diagnosing memory growth, mirroring the existing
app-proxy coordinator profiling setup.

- New shared `MemrayConfig` (common/configs/memray.py) with `enabled`,
  `output-destination`, and `native-traces`, so manager/storage can reuse it.
- The agent installs a `memray.Tracker(follow_fork=True, native_traces=...)`
  in `main()` before the service worker forks, and tears it down in the
  `finally` block. `follow_fork=True` captures the forked worker that does
  the real work; the capture path gets the PID appended so a restart does
  not collide with an earlier capture (memray refuses to overwrite).
- Disabled by default; enabling adds runtime/disk overhead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Yaminyam
Yaminyam requested a review from a team as a code owner July 13, 2026 08:05
Copilot AI review requested due to automatic review settings July 13, 2026 08:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added size:L 100~500 LoC comp:agent Related to Agent component comp:common Related to Common component labels Jul 13, 2026
@Yaminyam
Yaminyam marked this pull request as draft July 13, 2026 08:08
Extend the shared MemrayConfig to the manager, mirroring the agent.

- Add `memray` to BootstrapConfig (read in main() before the workers fork)
  and to ManagerUnifiedConfig (for the generated sample).
- Install `memray.Tracker(follow_fork=True, native_traces=...)` in main()
  before `aiotools.start_server()` so every forked worker is captured; tear
  it down in the finally block. PID is appended to the capture path so a
  restart does not collide with an earlier capture.
- Make the shared prod example path component-neutral (output.bin).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added the comp:manager Related to Manager component label Jul 13, 2026
Migrate the app-proxy coordinator and worker to the shared MemrayConfig
via a new `[profiling.memray]` table, non-breaking.

- Add nested `memray: MemrayConfig` to ProfilingConfig; consumers read
  `profiling.memray.{enabled,output_destination,native_traces}` (adds
  native-traces support to app-proxy).
- Keep `enable_memray` / `memray_output_destination` working as deprecated
  aliases; a model_validator folds them into `memray` when the new table is
  absent (an explicit `[profiling.memray]` wins).
- Regenerate the coordinator/worker samples. The coordinator sample also
  picks up previously un-regenerated schema fields (pool_recycle,
  pool_pre_ping, traefik client_ip_strategy) that were already merged.

Note: app-proxy uses snake_case TOML keys, so the shared memray table renders
its kebab serialization aliases (output-destination, native-traces); both
snake and kebab are accepted on load.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added the comp:app-proxy Related to App Proxy component label Jul 13, 2026
@Yaminyam Yaminyam changed the title feat(agent): add optional memray allocation tracking feat: add opt-in memray allocation tracking (agent, manager, app-proxy) Jul 13, 2026
@jopemachine

Copy link
Copy Markdown
Member

From what I remember, the memray option added to the App Proxy Coordinator was intended to be temporary and is expected to be removed. I think it's worth confirming whether that's still the plan. @HyeockJinKim

The worker's capture -- the only one worth reading, since the workers do the
real work -- was never closed, so it ended up truncated and unparseable: memray
stopped at the first bad record, reporting a 0.0s span out of a 366 MB file.

Two things line up to cause it:

- memray's `Tracker` writes the capture's trailer only from `__exit__`. It has
  no `__del__` and registers no `atexit` handler of its own.
- We only called `__exit__` in the master's `finally`, which never runs in a
  forked child.

An `atexit` handler in the child does not help either: `aiotools` ends its
forked children with a bare `os._exit()` (`aiotools/fork.py`), which runs
neither `atexit` handlers nor interpreter finalization. So the worker has to
close the tracker itself, explicitly, while it is still running Python.

Hoist the tracker lifecycle into two helpers next to the Pyroscope profiler --
`start_memray_tracker()` for the master, `close_memray_tracker_in_worker()` for
the child -- and call the latter from the shutdown path of each component's
forked entry point (agent, manager, app-proxy coordinator and worker, all of
which fork via `aiotools.start_server()`).

`Tracker.__exit__` is not idempotent (a second call raises), so the master and
each worker close exactly one tracker: their own. The module global is a
per-process copy after the fork, so the worker clearing it does not disturb the
master.

This also gives app-proxy the PID-suffixed capture path the agent and manager
already had, so a restart no longer collides with an earlier capture (memray
refuses to overwrite one).

Verified against a live agent: the forked worker's capture goes from a 0.0s span
that memray rejects with "Invalid record type" to a finalized 68.7s capture that
parses cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added size:XL 500~ LoC and removed size:L 100~500 LoC labels Jul 14, 2026
Extend the shared MemrayConfig to the storage proxy, matching the agent,
manager, and app-proxy. The storage proxy also forks its service worker via
`aiotools.start_server()`, so the same follow-fork capture applies.

- Add `memray` to StorageProxyUnifiedConfig (read in main() before the worker
  forks) and regenerate the sample.
- Install `memray.Tracker(follow_fork=True, ...)` in main() via the shared
  helper; the master closes it in the finally block and each forked worker
  closes its own capture from its shutdown path (aiotools ends children with a
  bare os._exit(), so atexit never runs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added the comp:storage-proxy Related to Storage proxy component label Jul 15, 2026
…ngelog

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Yaminyam Yaminyam changed the title feat: add opt-in memray allocation tracking (agent, manager, app-proxy) feat: add opt-in memray allocation tracking (agent, manager, storage-proxy, app-proxy) Jul 15, 2026
@Yaminyam
Yaminyam marked this pull request as ready for review July 15, 2026 04:39
@autoweave-bot

Copy link
Copy Markdown

Oh cool, this adds some really neat performance insights! 🚀😌
Explore here →

Subweave map of lablup/backend.ai#12800

Maintainer? Turn off weaves from non-maintainers →
Carefully crafted by Subweave · 🧶 used ~1.6m LLM tokens

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

Labels

comp:agent Related to Agent component comp:app-proxy Related to App Proxy component comp:common Related to Common component comp:manager Related to Manager component comp:storage-proxy Related to Storage proxy component size:XL 500~ LoC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants