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); }); }