Skip to content

feat(telemetry): report concurrency limit state (limit, active, queued, timeouts) - #5280

Merged
marcromeyn merged 1 commit into
mainfrom
romeyn/concurrency-state-main
Nov 4, 2026
Merged

marcromeyn merged 1 commit into
mainfrom
romeyn/concurrency-state-main

Conversation

@marcromeyn

@marcromeyn marcromeyn commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

What and why

A queue-wait duration shows that work waited, not why. To show "limit 100, active 100, queued 75", the telemetry dashboards need the state of each concurrency limit.

This PR adds MeteredSemaphore (nemo_gym/telemetry/concurrency.py), a drop-in asyncio.BoundedSemaphore that counts slots in use, waiters, and failed admissions with plain integers. The process observer exports them as observable instruments, read at export, so acquiring a slot makes no OpenTelemetry call:

Metric Meaning
gym.concurrency.limit / .active / .available configured, held and free slots
gym.concurrency.queue_depth callers waiting right now
gym.concurrency.admission_timeouts_total waits that ran out of the admission timeout (only for limits that have one)
gym.concurrency.admission_cancelled_total waits cancelled before a slot was free

Attributes are bounded: component, limit_name (one server can own several limits), and the server name. Missing stays missing: a component without a limit reports no series, not a limit of 0. The admission timeout moves into the semaphore, so a timeout is counted as a timeout, not as the cancellation asyncio.wait_for would make it look like.

Wired into the limits with the clearest owner:

Component limit_name Configured by Admission timeout
rollout driver rollouts num_samples_in_parallel none
openai_model (and litellm_model) upstream_requests max_concurrent_requests upstream_pool_timeout_seconds
BaseEnvironmentServer episodes max_concurrent_episodes queue_timeout_seconds
math_with_judge library_verifier library_verifier_max_concurrency none

The metrics page gains a "Concurrency Limit Metrics" section. Other agent, model, resources and sandbox limits follow as one constructor change each.

Relationship to #2935: this answers the same dashboard question and reuses its instrument names and module path, so its panels keep working. It is built on main because #2935's branch has no common ancestor with it, sets synchronous gauges on every acquire, and would count wait_for timeouts as cancellations. #2935's queue-wait histogram fits as one call in MeteredSemaphore.acquire once it is rebased.

Validation

  • At 245a8ce, with the telemetry extra: telemetry, environment-server, openai_model, litellm_model, rollout collection, reverification and docs-link tests: 1494 passed.
  • 20 new concurrency tests (exported state at several capacity states, timeouts vs cancellations, summing by attributes, garbage-collected limits, metrics off, no limit, over-release) plus wiring tests at each call site. Each fails without the production change (checked before the rebase).
  • asyncio hands a released slot straight to the next waiter, so a second release() before that waiter resumes would pass the base class's bound check; release() raises when no slot is held, and its regression test fails without that check.
  • pre-commit on the changed files; math_with_judge server tests (130 passed, before the rebase, which only touched an import line).

Rollout evidence

gym env start with openai_model (gpt-6-luna), console metrics exporter and max_concurrent_requests=1, then gym eval run --no-serve --limit 3 --concurrency 3: the model limit read limit 1, active 1, queue depth 2 → 1 → 0, and the driver limit active 3 draining to 0; 3/3 rollouts. Before the rebase, a run with upstream_pool_timeout_seconds=0.5 counted the two queued calls as 2 admission timeouts.

Compatibility

No configuration changes and new metrics only. The admission timeouts raise the same TimeoutError and return the same failure responses. Releasing a slot that is not held now raises ValueError; no call site does that.

Linear: FEP-1844 (part of FEP-1829 / GYM-1170).

@copy-pr-bot

copy-pr-bot Bot commented Oct 29, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@marcromeyn

Copy link
Copy Markdown
Contributor Author

/ok to test 245a8ce

@marcromeyn
marcromeyn enabled auto-merge October 29, 2026 10:23
@svcnvidia-nemo-ci svcnvidia-nemo-ci added area:core Shared APIs, servers, telemetry, health, and registries feature New capabilities, enhancements, or enablement work needs-review PR is ready for code review and waiting on a reviewer labels Oct 29, 2026
@nemo-automation-bot
nemo-automation-bot Bot requested review from a team October 29, 2026 10:24
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

@svcnvidia-nemo-ci svcnvidia-nemo-ci 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.

SHIP

LGTM — no concerns found.

Reviewed all 11 changed files and directly affected paths, including semaphore handoff and cancellation, admission-timeout handling, owner cleanup, metric aggregation and observer lifecycle, and documentation consistency. The double-release handoff regression is covered by the current guard and test.

Static review only; tests were not executed in this read-only environment.

Note: formal approval is not available for this request, so this review is posted as a comment only.

Review generated by codex. This was a full review. Request a full review with /review reset=true.

@github-actions github-actions Bot added the sla:review-overdue Review response is over the one-business-day SLA label Oct 30, 2026
@marcromeyn
marcromeyn force-pushed the romeyn/concurrency-state-main branch from 245a8ce to 7bc0817 Compare November 2, 2026 13:52
@marcromeyn

Copy link
Copy Markdown
Contributor Author

/ok to test 7bc0817

@marcromeyn

Copy link
Copy Markdown
Contributor Author

/ok to test 044d19a

…d, timeouts)

A queue-wait duration shows that work waited, not why. A telemetry
dashboard needs the state of each concurrency limit, "limit 100,
active 100, queued 75", to show that the limit is full and work is
piling up behind it.

Add MeteredSemaphore, an asyncio.BoundedSemaphore that counts its slots
in use, its waiters, and its failed admissions with plain integers. The
process observer exports them through observable instruments read at
export: gym.concurrency.limit, .active, .available and .queue_depth,
plus gym.concurrency.admission_timeouts_total and
.admission_cancelled_total. Points carry only bounded attributes:
component, limit name and, for servers, the server name. A component
without a configured limit reports no series instead of a limit of 0,
and the timeout counter exists only for limits that can time out.

The admission timeout moves into the semaphore so a timeout is counted
as a timeout rather than as a cancellation, as asyncio.wait_for would
make it look.

Wire it into the limits that exist today with the clearest owner: the
rollout driver (num_samples_in_parallel), openai_model and servers
built on it (max_concurrent_requests, upstream_pool_timeout_seconds),
the environment-server episode admission (max_concurrent_episodes,
queue_timeout_seconds) and math_with_judge's library verifier.

```
```
driver       [rollout_driver, rollouts]                   limit=1 active=1 available=0 queue_depth=2
driver       [rollout_driver, rollouts]                   limit=1 active=1 available=0 queue_depth=1
driver       [rollout_driver, rollouts]                   limit=1 active=1 available=0 queue_depth=0
driver       [rollout_driver, rollouts]                   limit=1 active=0 available=1 queue_depth=0
policy_model [model_server, upstream_requests, policy_model] limit=1 active=1 queue_depth=0 admission_timeouts_total=0
```
```
driver       [rollout_driver, rollouts]                   limit=3 active=3 available=0 queue_depth=0
policy_model [model_server, upstream_requests, policy_model] limit=1 active=1 available=0 queue_depth=2
policy_model [model_server, upstream_requests, policy_model] limit=1 active=1 available=0 queue_depth=1
policy_model [model_server, upstream_requests, policy_model] limit=1 active=0 available=1 queue_depth=0
```
```
policy_model [model_server, upstream_requests, policy_model] limit=1 active=1 queue_depth=2 admission_timeouts_total=0
policy_model [model_server, upstream_requests, policy_model] limit=1 active=0 queue_depth=0 admission_timeouts_total=2

Signed-off-by: Marc Romeyn <mromeijn@nvidia.com>
@marcromeyn
marcromeyn force-pushed the romeyn/concurrency-state-main branch from 044d19a to 20e449f Compare November 4, 2026 11:05
@marcromeyn

Copy link
Copy Markdown
Contributor Author

/ok to test 20e449f

@marcromeyn
marcromeyn added this pull request to the merge queue Nov 4, 2026
@github-actions github-actions Bot removed the sla:review-overdue Review response is over the one-business-day SLA label Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/41876809199

Merged via the queue into main with commit d56eeb5 Nov 4, 2026
76 of 79 checks passed
@marcromeyn
marcromeyn deleted the romeyn/concurrency-state-main branch November 4, 2026 17:49

This branch was previously deployed

1 inactive deployment
public — 20e449f9 Deployed Nov 4, 2026 by marcromeyn via release / finalize / notify #9957
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core Shared APIs, servers, telemetry, health, and registries feature New capabilities, enhancements, or enablement work needs-review PR is ready for code review and waiting on a reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants