Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bindings/pycvc/pycvc.i
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion bindings/pycvc/test_pycvc_state_exec_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down
4 changes: 2 additions & 2 deletions docs/ARIADNE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/STATE_EXEC_DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<id>.limits.async_steps_per_cpu_ms`,
Expand All @@ -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).
Expand Down
22 changes: 12 additions & 10 deletions docs/STATE_EXEC_EXCEPTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,14 +474,16 @@ limits what that process can make the app do for it.
default);
2. `state_exec.schedulers.<id>.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
Expand Down Expand Up @@ -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 |
Expand All @@ -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.<id>.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 |
Expand Down
10 changes: 6 additions & 4 deletions inc/cvc/state/state_exec/scheduler_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.<scheduler_id>.limits.async_steps_per_cpu_ms` (when scheduler_id is
Expand Down
12 changes: 7 additions & 5 deletions src/cvc/tests/state_exec_async_calls_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down Expand Up @@ -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
});
}

Expand Down Expand Up @@ -893,8 +894,9 @@ TEST(AsyncCalls, AS27_TheStepRateIsReadAtEveryCharge) {
const value_t v = parse(o.value);
const auto &l = *std::get<list_ptr>(v.v);
const int64_t first = std::get<int64_t>(l[0].v), second = std::get<int64_t>(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);
});
}
Expand Down
Loading