From 7556c7d26e2a6c4b3278975945586c44dab34607 Mon Sep 17 00:00:00 2001 From: Joe Rivera Date: Tue, 27 Oct 2026 23:19:09 -0500 Subject: [PATCH] state_exec: the default async step rate is 8000, re-measured after the evaluator speedup StepCostSetsTheDefaultRate holds kDefaultAsyncStepsPerCpuMs within a factor of four of what a step costs. Since the evaluator's hot-path work, steps cost a quarter of what the 2000 default was set from, and the test fails on macOS arm64 (2000 vs 2026) and on MSVC x64. On master c0e0b5ac (MSVC Release, x64), the async scheduler measures 7,700 to 11,400 steps per CPU ms on a lightly loaded box; a macOS arm64 runner measured 8,100 to 8,700. 8000 rounds that down. The tests that hard-coded counts from 2000 (AS18/AS26, AS21, AS27, pycvc's sleeping call) use the constant instead, and the docs follow. Ariadne's 5000-step action cap now covers 0.625 ms of async kernel CPU. --- bindings/pycvc/pycvc.i | 2 +- bindings/pycvc/test_pycvc_state_exec_async.py | 2 +- docs/ARIADNE.md | 4 ++-- docs/STATE_EXEC_DEVELOPER_GUIDE.md | 4 ++-- docs/STATE_EXEC_EXCEPTIONS.md | 22 ++++++++++--------- inc/cvc/state/state_exec/scheduler_base.h | 10 +++++---- src/cvc/tests/state_exec_async_calls_test.cpp | 12 +++++----- 7 files changed, 31 insertions(+), 25 deletions(-) diff --git a/bindings/pycvc/pycvc.i b/bindings/pycvc/pycvc.i index 9dead71a8..f93101e30 100644 --- a/bindings/pycvc/pycvc.i +++ b/bindings/pycvc/pycvc.i @@ -3813,7 +3813,7 @@ public: static data_options host_data_defaults(); }; PYCVC_STATE_EXEC_SCHEDULER_EXTEND(async_scheduler) -const double kDefaultAsyncStepsPerCpuMs = 2000.0; +const double kDefaultAsyncStepsPerCpuMs = 8000.0; double async_steps_per_cpu_ms_from_state(cvc::state &root, const std::string &scheduler_id); void publish_async_steps_per_cpu_ms(cvc::state &root, const std::string &scheduler_id, double rate); diff --git a/bindings/pycvc/test_pycvc_state_exec_async.py b/bindings/pycvc/test_pycvc_state_exec_async.py index f3dc4d454..cf849fde9 100644 --- a/bindings/pycvc/test_pycvc_state_exec_async.py +++ b/bindings/pycvc/test_pycvc_state_exec_async.py @@ -497,7 +497,7 @@ def test_a_sleeping_call_is_charged_its_run_time_but_no_cpu(): i = w.info(w.run("(slow-op 1 200)")) assert i.async_time >= 0.19 assert i.async_cpu_time < 0.05 - assert 1 <= i.async_steps < 100000 + assert 1 <= i.async_steps < 50 * pycvc.kDefaultAsyncStepsPerCpuMs # under 50 CPU ms w.close() diff --git a/docs/ARIADNE.md b/docs/ARIADNE.md index bdeec4cdf..37c0ee3b7 100644 --- a/docs/ARIADNE.md +++ b/docs/ARIADNE.md @@ -137,8 +137,8 @@ default. The step and time caps cover the action's async calls (ยง10.1; developer guide, "Async Functions"): a call's run time and CPU time count against the time cap, and its CPU time as steps -at `limits.async_steps_per_cpu_ms` (2000 per CPU millisecond by default). At that rate the -5000-step cap is 2.5 ms of a kernel's CPU, so a host whose documents call CPU-heavy async verbs +at `limits.async_steps_per_cpu_ms` (8000 per CPU millisecond by default). At that rate the +5000-step cap is 0.625 ms of a kernel's CPU, so a host whose documents call CPU-heavy async verbs raises the caps. An action that runs out is killed, and the kernel sees `cancelled()`. The memory budget covers the action's state-tree writes and its diff --git a/docs/STATE_EXEC_DEVELOPER_GUIDE.md b/docs/STATE_EXEC_DEVELOPER_GUIDE.md index f6cef412e..7d315496e 100644 --- a/docs/STATE_EXEC_DEVELOPER_GUIDE.md +++ b/docs/STATE_EXEC_DEVELOPER_GUIDE.md @@ -1180,7 +1180,7 @@ work (`docs/STATE_EXEC_EXCEPTIONS.md` 3.8): first half of `max_time`, and its **CPU time** (its thread's, plus its `computePool().parallel_for` chunks') toward the second. - **Steps:** the CPU time at `limits.async_steps_per_cpu_ms` steps per CPU - millisecond (default 2000; at least one step per call), against `max_steps`. + millisecond (default 8000; at least one step per call), against `max_steps`. The rate is read live; the first of these that is set wins: `execute_options::async_steps_per_cpu_ms`, `state_exec.schedulers..limits.async_steps_per_cpu_ms`, @@ -1195,7 +1195,7 @@ work (`docs/STATE_EXEC_EXCEPTIONS.md` 3.8): `async_steps`. A short budget limits what a call can do: at the default rate, 5000 steps are -2.5 ms of CPU. Ariadne's actions run with `ariadne.limits.action_max_steps` +0.625 ms of CPU. Ariadne's actions run with `ariadne.limits.action_max_steps` (5000) and `ariadne.limits.action_max_time` (0.1 s), app-wide settings a document can't change, so a host whose documents call heavy async verbs raises them (`docs/ARIADNE.md` 2.2). diff --git a/docs/STATE_EXEC_EXCEPTIONS.md b/docs/STATE_EXEC_EXCEPTIONS.md index 5cc48cfe0..207e5e5bf 100644 --- a/docs/STATE_EXEC_EXCEPTIONS.md +++ b/docs/STATE_EXEC_EXCEPTIONS.md @@ -474,14 +474,16 @@ limits what that process can make the app do for it. default); 2. `state_exec.schedulers..limits.async_steps_per_cpu_ms`; 3. `state_exec.defaults.limits.async_steps_per_cpu_ms`; - 4. the built-in default, 2000. P3 measured what a step costs and set it from that: about - 0.4 to 0.45 CPU microseconds on the async scheduler (Ariadne's and `Exec`'s) and 0.26 - to 0.3 on the sync one, on a loop of arithmetic, DSL function calls and list and dict - work (MSVC Release, x64; `StepCostSetsTheDefaultRate` in - `state_exec_async_calls_test`, which, in an optimized build, fails if the default - drifts beyond a factor of four of either; a debug build's steps cost 20 to 40 times - more). 2000 rounds the async scheduler's 2,200 to 2,500 steps per CPU - millisecond down. (The first draft's 1000 was provisional.) + 4. the built-in default, 8000. It is set from what a step costs: since the evaluator + work of transfix/libcvc#844, about 0.09 to 0.13 CPU microseconds on the async scheduler + (Ariadne's and `Exec`'s) and 0.08 to 0.1 on the sync one, on a loop of arithmetic, DSL + function calls and list and dict work (MSVC Release, x64, 2026-10-28; + `StepCostSetsTheDefaultRate` in `state_exec_async_calls_test`, which, in an optimized + build, fails if the default drifts beyond a factor of four of either; a debug build's + steps cost 20 to 40 times more). 8000 rounds the async scheduler's 7,700 to 11,400 + steps per CPU millisecond down; a macOS arm64 runner measured 8,100 to 8,700. (P3 set + 2000 from the 2,200 to 2,500 that steps cost before #844; the first draft's 1000 was + provisional.) - The value is a non-negative decimal number; a fraction is allowed, `0.5` being a step every 2 CPU ms. A node that isn't one falls through to the next level. - The scheduler publishes the effective value under its own subtree, as it does its other @@ -834,7 +836,7 @@ them into gtest and pytest cases with these IDs. | AS-15 | a snapshot of a process parked in a call, restored | raises `RuntimeError` at the call (the call can't resume) | | AS-16 | `Exec.register_async_fn` raising | raises at the call site, no `__async_error__` dict (the compatibility change) | | AS-17 | an async function called from Ariadne's reactive lane | not reachable (action seam only); if called, `RuntimeError` | -| AS-18 | a 200 ms CPU-bound kernel | `async_calls` 1, `async_time` and `async_cpu_time` about 0.2 s, `async_steps` about 400,000 at the default rate; `elapsed_time` includes the 0.2 s | +| AS-18 | a 200 ms CPU-bound kernel | `async_calls` 1, `async_time` and `async_cpu_time` about 0.2 s, `async_steps` about 1,600,000 at the default rate; `elapsed_time` includes the 0.2 s | | AS-19 | `max_time` 0.1 s, a 1 s CPU-bound kernel that checks `cancelled()`, the call inside a `try` | `killed`, `time_limit_exceeded`, well before 1 s; the catch didn't run; the kernel sees `cancelled()` | | AS-20 | `max_steps` 1000, a 50 ms CPU-bound kernel | `killed`, `max_steps_exceeded`; with `limits.async_steps_per_cpu_ms` 0, it finishes | | AS-21 | a kernel that sleeps 200 ms (blocking, almost no CPU) | `async_time` about 0.2 s, charged to `max_time`; `async_cpu_time` near 0; a step or a few | @@ -843,7 +845,7 @@ them into gtest and pytest cases with these IDs. | AS-24 | two futures in flight from one process, 100 ms each | `async_time` about 0.2 s, though about 0.1 s passed | | AS-25 | a kernel that never checks `cancelled()`, past `max_time` | the process is killed when its run time reaches the limit; the late result is dropped | | AS-26 | the AS-18 kernel inline (sync scheduler) | the same charges as AS-18, not the step's capped charge | -| AS-27 | two 50 ms kernels in a row; between them `state_exec.defaults.limits.async_steps_per_cpu_ms` is set to `0`, with no `load_settings()` | the first call is charged about 100,000 steps and the second one | +| AS-27 | two 50 ms kernels in a row; between them `state_exec.defaults.limits.async_steps_per_cpu_ms` is set to `0`, with no `load_settings()` | the first call is charged about 400,000 steps and the second one | | AS-28 | the rate set to 10 in `state_exec.defaults.limits` and to 100 in `state_exec.schedulers..limits` | a 50 ms kernel is charged about 5,000 steps (the scheduler's node wins); with `execute_options::async_steps_per_cpu_ms` 1, about 50 | | AS-29 | AS-21's sleeping kernel, on Windows | `async_cpu_time` near 0 and a step or a few, as on Linux and macOS (the thread CPU clock) | | AS-30 | `max_memory` 1 MiB, a kernel that reports allocating 4 MiB through `eval_alloc_check`, the call inside a `try` | `MemoryError` at the call site, caught; `heap_alloc_bytes` grew by what the kernel reported before the refusal | diff --git a/inc/cvc/state/state_exec/scheduler_base.h b/inc/cvc/state/state_exec/scheduler_base.h index 25063477e..dd22103f0 100644 --- a/inc/cvc/state/state_exec/scheduler_base.h +++ b/inc/cvc/state/state_exec/scheduler_base.h @@ -211,10 +211,12 @@ class scheduler_base { }; /// limits.async_steps_per_cpu_ms when nothing sets it: what the steps cost, a step being about -/// 0.4 to 0.45 CPU microseconds on the async scheduler and 0.26 to 0.3 on the sync one (a loop of -/// arithmetic, DSL function calls, list and dict work; MSVC Release, x64, 2026-10-24: the -/// StepCostSetsTheDefaultRate case of state_exec_async_calls_test), rounded down. -inline constexpr double kDefaultAsyncStepsPerCpuMs = 2000.0; +/// 0.09 to 0.13 CPU microseconds on the async scheduler and 0.08 to 0.1 on the sync one (a loop +/// of arithmetic, DSL function calls, list and dict work; MSVC Release, x64, 2026-10-28, after +/// the evaluator's hot-path work: the StepCostSetsTheDefaultRate case of +/// state_exec_async_calls_test), rounded down. A macOS arm64 runner measured 8,100 to 8,700 steps +/// per CPU millisecond. +inline constexpr double kDefaultAsyncStepsPerCpuMs = 8000.0; /// limits.async_steps_per_cpu_ms from the state tree under `root`: /// `state_exec.schedulers..limits.async_steps_per_cpu_ms` (when scheduler_id is diff --git a/src/cvc/tests/state_exec_async_calls_test.cpp b/src/cvc/tests/state_exec_async_calls_test.cpp index dcf0d448a..c615d1d6e 100644 --- a/src/cvc/tests/state_exec_async_calls_test.cpp +++ b/src/cvc/tests/state_exec_async_calls_test.cpp @@ -771,8 +771,9 @@ TEST(AsyncCalls, AS18_AS26_ACpuBoundCallsCharges) { EXPECT_GE(i.async_cpu_time, 0.19); EXPECT_LT(i.async_cpu_time, 0.4); EXPECT_GE(i.async_time, i.async_cpu_time - 0.02); - EXPECT_GE(i.async_steps, 360000u); // 0.2 CPU s at the default 2000 steps per CPU ms - EXPECT_LT(i.async_steps, 800000u); + const double at_default = 200.0 * kDefaultAsyncStepsPerCpuMs; // 0.2 CPU s at the default + EXPECT_GE(double(i.async_steps), 0.9 * at_default); + EXPECT_LT(double(i.async_steps), 2.0 * at_default); EXPECT_GE(i.step_count, i.async_steps); // they count in step_count EXPECT_GE(i.elapsed_time, i.async_time - 1e-3); }); @@ -826,7 +827,7 @@ TEST(AsyncCalls, AS21_AS29_ASleepingCallIsChargedItsRunTimeButNoCpu) { EXPECT_GE(i.elapsed_time, 0.19); // charged to max_time's first half EXPECT_LT(i.async_cpu_time, 0.03); EXPECT_GE(i.async_steps, 1u); - EXPECT_LT(i.async_steps, 60000u); + EXPECT_LT(double(i.async_steps), 30.0 * kDefaultAsyncStepsPerCpuMs); // under 30 CPU ms }); } @@ -893,8 +894,9 @@ TEST(AsyncCalls, AS27_TheStepRateIsReadAtEveryCharge) { const value_t v = parse(o.value); const auto &l = *std::get(v.v); const int64_t first = std::get(l[0].v), second = std::get(l[1].v); - EXPECT_GE(first, 80000); // 50 CPU ms at the default 2000 steps per CPU ms - EXPECT_LT(first, 200000); + const double at_default = 50.0 * kDefaultAsyncStepsPerCpuMs; // 50 CPU ms at the default + EXPECT_GE(double(first), 0.8 * at_default); + EXPECT_LT(double(first), 2.0 * at_default); EXPECT_EQ(second, 1); }); }