dau_scheduler¶
Submodules¶
Attributes¶
Exceptions¶
A posture was requested that cannot be resolved into a share. |
|
A split was asked for that cannot be executed or merged as asked. |
Classes¶
What one query's partials are, and whether they can be put together. |
|
Anything that can price a row count on the engine the caller is on. |
|
How one query's rows divide between the two engines. |
|
The measured host-to-device transfer rate of one link: the term the |
|
One plan node's measured cost, and whether it scales with rows. |
|
Profiles on disk, keyed by plan and host, and link rates keyed by |
|
A measured host cost for one plan on one host. |
|
Rows the caller has already materialized: the offsets AND the values. |
|
Rows named by their offsets in the table, and nothing else. |
|
The rows one arm owns, named by offset and optionally by value. |
|
One executed split: what each engine ran, how long, and the answer. |
Functions¶
|
Refuse a query whose result cannot be reassembled, by name. |
|
Merge per-key records that carry no ordering, over the whole key set. |
|
Concatenate per-group records, folding the group that straddles the |
|
Merge the arms' sorted runs into one sorted run. |
|
Re-rank the arms' k-lists into one. |
|
Add the named aggregates, then RE-DERIVE anything computed from them. |
|
How many batches one engine's share should be delivered in. |
|
Resolve a posture into a row split. |
|
A name for the machine a profile was measured on. |
|
Build a profile from an engine's per-node timings. |
|
The seam's argument, from whatever the caller is holding. |
|
Run |
Package Contents¶
- dau_scheduler.__version__ = '0.1.0'¶
- exception dau_scheduler.PostureError[source]¶
Bases:
ValueErrorA posture was requested that cannot be resolved into a share.
Initialize self. See help(type(self)) for accurate signature.
- exception dau_scheduler.SplitError[source]¶
Bases:
ValueErrorA split was asked for that cannot be executed or merged as asked.
Initialize self. See help(type(self)) for accurate signature.
- dau_scheduler.ADDITIVE: frozenset[str]¶
- dau_scheduler.AGGREGATE_REFUSALS: dict[str, str]¶
- dau_scheduler.BOUNDARY_STATS: frozenset[str]¶
- dau_scheduler.DEFERRED_MEAN: frozenset[str]¶
- class dau_scheduler.MergeRule[source]¶
What one query’s partials are, and whether they can be put together.
terminalnames the result shape, for messages.combinetakes the arms’ partials IN ARM ORDER – the leading rows first – and returns the answer the unsplit query would have produced.mergeableis the set of aggregate kinds whose partials this shape can combine, consulted bycheck_split()and default-deny.refusal, when set, says this shape cannot be split at all, and nocombineis reached.failedis the guard for an arm that could not complete. An engine that invalidated its own work returns a partial that says so, and merging that into the other arm’s would produce a plausible SHORT answer – so if any partial reports failure, the merged result is that partial, failure and all, rather than a total nobody can tell is missing rows.- terminal: str¶
- combine: collections.abc.Callable[[collections.abc.Sequence[Any]], Any]¶
- mergeable: frozenset[str]¶
- failed: collections.abc.Callable[[Any], bool] | None = None¶
- refusal: str | None = None¶
- dau_scheduler.check_split(rule: MergeRule, aggregates: collections.abc.Iterable[tuple[str, str]] = ()) None[source]¶
Refuse a query whose result cannot be reassembled, by name.
aggregatesare(output name, aggregate kind)pairs, so the refusal names the output the caller has to change rather than only the kind.Kept separate from execution so a planner can ask whether a query is splittable at all before committing anything to either engine.
- dau_scheduler.merge_keyed_groups(partials: collections.abc.Sequence[collections.abc.Sequence[Any]], *, key: collections.abc.Callable[[Any], Any], fold: collections.abc.Callable[[Any, Any], Any]) list[Any][source]¶
Merge per-key records that carry no ordering, over the whole key set.
The contrast with
merge_ordered_groups()is the point. An engine holding one accumulator per distinct key can meet a key anywhere in its rows, so a key may appear in both arms and there is no boundary pair to fold – every key has to be looked up. The result is key-ascending, which is the order such an engine drains in.
- dau_scheduler.merge_ordered_groups(partials: collections.abc.Sequence[collections.abc.Sequence[Any]], *, key: collections.abc.Callable[[Any], Any], fold: collections.abc.Callable[[Any, Any], Any]) list[Any][source]¶
Concatenate per-group records, folding the group that straddles the boundary.
Valid only when the rows are SORTED on the group key, because that is what makes the boundary pair the only group two arms can share. The precondition is verified rather than trusted: on unsorted rows an order-sensitive engine emits several records per key, and merging those by key would collapse them into an answer no run produced.
foldcombines two partial records of the same group;keyreads a record’s group key.
- dau_scheduler.merge_sorted_runs(partials: collections.abc.Sequence[collections.abc.Sequence[Any]], *, key: collections.abc.Callable[[Any], Any], descending: bool = False) list[Any][source]¶
Merge the arms’ sorted runs into one sorted run.
Equal keys take the earlier range first, which is what keeps a stable sort’s arrival-order ties true of the whole table rather than of each half.
- dau_scheduler.merge_top_k(partials: collections.abc.Sequence[collections.abc.Sequence[Any]], *, limit: int, key: collections.abc.Callable[[Any], Any], descending: bool = True) list[Any][source]¶
Re-rank the arms’ k-lists into one.
A row in the whole query’s top k ranks at least as high within the range it arrived in, so it is in that range’s k-list: the arms’ outputs are the complete candidate set. Each arm emits equal keys in arrival order and the arms are given in row order, so a STABLE re-rank keeps an earliest-arrival tie-break true of the whole table rather than of each half.
- dau_scheduler.merge_totals(partials: collections.abc.Sequence[dict[str, Any]], names: collections.abc.Sequence[str], *, finalize: collections.abc.Callable[[dict[str, Any]], Any] | None = None) Any[source]¶
Add the named aggregates, then RE-DERIVE anything computed from them.
finalizeis where an output that is an EXPRESSION over the aggregates – a ratio, a share, a rate – is recomputed from the merged totals. Adding the two arms’ ratios would produce a number that is not a ratio of anything, and it would look exactly like a total.
- dau_scheduler.POSTURES: tuple[str, ...] = ('cpu', 'offload', 'collaborative', 'adaptive')¶
- class dau_scheduler.HostCost[source]¶
Bases:
ProtocolAnything that can price a row count on the engine the caller is on.
dau_scheduler.profile.QueryProfileis one, measured rather than modelled; a caller with an analytic model of its own host can pass that instead.
- dau_scheduler.Posture¶
- class dau_scheduler.WorkSplit(/, **data: Any)[source]¶
Bases:
pydantic.BaseModelHow one query’s rows divide between the two engines.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
- model_config: ClassVar¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- posture: str¶
- rationale: str¶
- predicted_speedup: float | None = None¶
- check() WorkSplit[source]¶
Re-run the invariants and return
self.Construction validates, but
model_copy(update=...)does not, so every consumer calls this before acting on a split rather than trusting that the instance it was handed went through__init__.
- dau_scheduler.batch_count(payload_bytes: int, *, resident: bool, knee_bytes: int) int[source]¶
How many batches one engine’s share should be delivered in.
Batching exists to hide the transfer behind the compute. Each batch’s transfer overlaps the previous batch’s work, so the pipelined cost falls toward the transfer time alone. With the data already RESIDENT on the engine there is no transfer to hide and every batch is pure per-call overhead, so the answer is one.
Measured on one link with a 45.8 MiB payload:
batches
streamed
resident
1
22.08 ms
5.27 ms
3
18.78
5.91 ms
12
22.59 ms
–
The optima are opposite, so a single tuned batch count is wrong for one of them – which is why this takes
residentrather than a constant.Streamed, the count targets the link’s transfer knee rather than a fitted model.
knee_bytesis the payload below which a transfer loses rate to per-transfer setup; it is a property of the link and has to be measured on it, so there is no default here. FittingT(B) = transfer + work/B + c*Bto the measured points above givescanywhere from 83 to 568 microseconds depending on which pair is fitted, so the model does not generalize and the measured knee is the honest thing to encode. At a 15 MiB knee, 45.8 MiB rounds to 3, which is what measured fastest.
- dau_scheduler.plan_split(posture: Posture = 'collaborative', *, cpu_seconds: float | None = None, device_seconds: float | None = None, profile: HostCost | None = None, rows: int | None = None, device_fixed_seconds: float = 0.0) WorkSplit[source]¶
Resolve a posture into a row split.
cpu_secondsanddevice_secondsare whole-query estimates for each engine alone. Onlycollaborativeneeds them; the other postures are definitional and take none, which is the point of offering them – a caller who does not want to pay for cost estimation can still express an intent.The host figure may instead come from a measurement: pass a
HostCostand therowsthis run will process, and the estimate is taken from it. An explicitcpu_secondsstill wins – a caller who measured the thing itself should not be overridden by a model of it.What is refused is guessing. With neither a figure nor a profile,
collaborativedeclines to invent one, because a fabricated number underneath a real decision is worse than no decision: the split changes which engine runs which rows, so an error in it is self-reinforcing.device_fixed_secondsis what one device job costs before it has processed a row – launching it, polling it, reading its records – a per-invocation term the proportional form omitted and one measurement put at a few milliseconds beside a 12.3 ms host time (2026-09-27). With it the balance is(1 - s) * cpu = fixed + s * device, and a share that cannot beat the host alone (the fixed cost is already most of the host time) is acpusplit that says why, not a split predicted to win by a margin the fixed cost eats.
- class dau_scheduler.LinkRate(/, **data: Any)[source]¶
Bases:
pydantic.BaseModelThe measured host-to-device transfer rate of one link: the term the adaptive cut model and a streamed split price every byte crossing with.
Measured, not declared: a platform states its bus width and lane count, but what a transfer actually sustains depends on the host, the slot, the driver and the transfer size, and the sweeps of record put the same card at different rates in different hosts. So the rate is keyed by (host, device), like a host profile, and measured where it will be used.
transfer_bytesis part of the identity, not a note: a link’s rate at one transfer size says little about another (per-transfer setup makes small transfers materially slower), so a rate is measured at the size the caller will move and cached under it.sampleskeeps the individual rates so the spread is visible beside the figure the model uses.measured_at(seconds since the epoch) is what staleness is judged by: a driver change, a link renegotiation or a reboot cannot invalidate an entry, so an old one is re-measured rather than trusted.Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
- model_config: ClassVar¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- host: str¶
- device: str¶
- bytes_per_second: float¶
- transfer_bytes: int¶
- samples: tuple[float, ...] = ()¶
- measured_at: float = 0.0¶
- is_stale(*, now: float, max_age_seconds: float) bool[source]¶
Older than
max_age_secondsatnow, or never dated.
- check() LinkRate[source]¶
Re-run the invariants and return
self(seeQueryProfile.check()).
- class dau_scheduler.NodeCost(/, **data: Any)[source]¶
Bases:
pydantic.BaseModelOne plan node’s measured cost, and whether it scales with rows.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
- model_config: ClassVar¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- node: str¶
- seconds: float¶
- scales_with_rows: bool¶
- check() NodeCost[source]¶
Re-run the invariants and return
self(seeQueryProfile.check()).
- class dau_scheduler.ProfileCache(root: pathlib.Path | str)[source]¶
Profiles on disk, keyed by plan and host, and link rates keyed by device and host.
Deliberately a plain directory of JSON rather than anything cleverer: a profile is small, human-readable, and worth being able to delete by hand when it goes stale.
The engine version is deliberately not part of the key. A host is a key because you cannot re-measure on a machine you are not on; an engine version is not, because you can always re-measure under the one you have. So it is staleness (
QueryProfile.is_stale_for()), handled exactly as a row count that has moved too far, rather than a second slot on disk.- root¶
- _under_root(name: str) pathlib.Path[source]¶
A file directly under the cache root. Every key component is percent-encoded with no safe characters, so a plan key or host that contains a slash,
..or a space becomes one flat, reversible file name and cannot name a path outside the root. The check is kept anyway: a cache that writes where it is told is the failure mode this guards.
- get(plan_key: str, host: str | None = None) QueryProfile | None[source]¶
- put(profile: QueryProfile) pathlib.Path[source]¶
- class dau_scheduler.QueryProfile(/, **data: Any)[source]¶
Bases:
pydantic.BaseModelA measured host cost for one plan on one host.
selectivityis the fraction of rows surviving the query’s filters when the caller measured it. It is leftNonerather than inferred: nothing in a timing profile reveals row counts, and a guessed selectivity poisons everything downstream that prices bytes.engine_versionrecords which build of the engine was measured. It defaults to""– unknown – and NOT to whatever is installed, which would let a hand-written profile claim to have been measured under an engine it never ran on.Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
- model_config: ClassVar¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- plan_key: str¶
- host: str¶
- rows: int¶
- fixed_seconds: float¶
- per_row_seconds: float¶
- selectivity: float | None = None¶
- engine_version: str = ''¶
- check() QueryProfile[source]¶
Re-run the invariants and return
self.Construction validates, but
model_copy(update=...)does not, so every use site (estimate,is_stale_for, the cache) calls this rather than trusting that the instance went through__init__.
- _costs_are_sane() QueryProfile[source]¶
- is_stale_for(rows: int, *, engine_version: str, factor: float = 10.0) bool[source]¶
Whether this measurement can still price
rowson the engine here now.Two things invalidate a measurement, and both are answered by this one predicate on purpose: a caller that has to remember to ask a second question is how a version stamp comes to be recorded on every profile and consulted by nothing.
Row count. Extrapolating one measurement across orders of magnitude is where a profile-guided split goes confidently wrong, and confidently wrong is worse than absent: the split decides which engine runs which rows, so the error is self-reinforcing.
Engine version. The argument for keying on the host – two machines measured 1.41x apart, so a profile keyed by plan alone steers the split on the wrong one – applies verbatim to the engine being measured, and this module exists BECAUSE an analytic model of that engine would be guesswork. A measurement taken under one build says nothing about another.
A profile with no recorded version is unknown, not current, and is stale for the same reason a guessed selectivity is refused: it may have been measured under anything.
- dau_scheduler.profile_from_spans(spans: collections.abc.Iterable[tuple[int, int, str]], *, rows: int, plan_key: str, host: str | None = None, is_fixed: collections.abc.Callable[[str], bool] = lambda _node: ..., selectivity: float | None = None, engine_version: str = '') QueryProfile[source]¶
Build a profile from an engine’s per-node timings.
spansare(start, end, node name)in MICROSECONDS, as a profiling query engine reports them.is_fixednames the plan-time nodes – the ones that must not be divided by the row count.Nodes overlap (an engine runs some in parallel) and leave gaps, so summing ``end - start`` is NOT elapsed time. It double-counts the overlap and drops the gaps entirely: with optimization 0-1000, scan 1000-11000 and filter 5000-15000, the true elapsed cost is 15 ms and the sum reports 21 ms.
So the span is taken as elapsed wall time, the fixed nodes are measured as a UNION of their intervals, and everything else – including the gaps, which are real elapsed cost – is attributed to scaling work.
- class dau_scheduler.RowBatch[source]¶
Bases:
RowSourceRows the caller has already materialized: the offsets AND the values.
The rows are WRAPPED rather than copied. Whatever normalization an arm performs on its own input is not repeated here, because repeating it over the whole table was the cost this seam exists to remove.
- rows: collections.abc.Sequence[Any]¶
- start: int = 0¶
- property stop: int¶
- class dau_scheduler.RowRange[source]¶
Bases:
RowSourceRows named by their offsets in the table, and nothing else.
The source a split between two engines that each hold the data wants: it costs two integers no matter how many rows it names, so a sweep over millions of rows allocates nothing per call.
- start: int¶
- stop: int¶
- class dau_scheduler.RowSource[source]¶
The rows one arm owns, named by offset and optionally by value.
- __slots__ = ()¶
- start: int¶
- stop: int¶
- dau_scheduler.as_row_source(rows: RowSource | collections.abc.Iterable[Any]) RowSource[source]¶
The seam’s argument, from whatever the caller is holding.
A source passes through untouched – that is the point, and the reason this is not
list(rows)under a better name. A sequence is wrapped in place, so a caller who already has the rows pays nothing to hand them over. Only an ITERATOR is drained, and it has to be: both arms read from one source, and an iterator cannot serve two.
- dau_scheduler.SplitArm¶
- class dau_scheduler.SplitExecution(/, **data: Any)[source]¶
Bases:
pydantic.BaseModelOne executed split: what each engine ran, how long, and the answer.
resultis whatever an UNSPLIT run of this query returns, so the caller’s decode path is unchanged by having split the work.overlap_secondsis the intersection of the two arms’ wall-clock intervals. It measures that the arms were IN FLIGHT together, which is the property a split needs and the one a serialized executor loses. It does not claim parallel progress inside a single interpreter, which nothing here could promise on the caller’s behalf.Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
- model_config: ClassVar¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- posture: str¶
- terminal: str¶
- device_rows: int¶
- host_rows: int¶
- device_seconds: float¶
- host_seconds: float¶
- wall_seconds: float¶
- overlap_seconds: float¶
- materialize_seconds: float¶
- merge_seconds: float¶
- device_granularity: int = 1¶
- result: Any = None¶
- _figures_are_sane() SplitExecution[source]¶
- dau_scheduler.execute_split(query: Any, rows: dau_scheduler.rows.RowSource | collections.abc.Iterable[Any], split: dau_scheduler.posture.WorkSplit, merge: dau_scheduler.merge.MergeRule, *, device: SplitArm, host: SplitArm, build_rows: dau_scheduler.rows.RowSource | collections.abc.Iterable[Any] | None = None, aggregates: collections.abc.Iterable[tuple[str, str]] = ()) SplitExecution[source]¶
Run
split’s share of the rows on one engine and the rest on the other, at the same time, and merge the partials into the query’s answer.queryis passed to both arms untouched; this module never inspects it.The
devicearm takes rows[0, share*n)and thehostarm the rest. That order is part of the contract, not an implementation detail: a groupedfirst/lastand a top-k’s earliest-arrival tie-break are defined by it. An arm with no rows is not launched at all – a share of 0.0 or 1.0 runs exactly the unsplit path (given a table on the device’s granularity), which is what makes the degenerate ends of a share sweep meaningful.rowsis the whole table as aRowSource– aRowRangewhen both arms run from data they already hold, aRowBatchwhen one of them reads values – or any iterable of rows, which is wrapped and charged tomaterialize_seconds.aggregatesare(output name, aggregate kind)pairs checked againstmergebefore anything runs (seedau_scheduler.merge.check_split()).A
devicearm carryingrow_granularity(seeSplitArm) can only take a row count that divides it – an engine whose job length must sit on a fixed row grid. The boundary is snapped to the nearest such multiple and the count actually given is reported asdevice_rows. It is an execution fact about the engine, not part of the share the planner chose, which is why it rides the arm rather than theWorkSplit.