bench: read the licence tier through license_info()

The sweep harness and ci_activate.py read the tier with the public
mbt.license_info(), and the parity-only check uses the published
Community limit of 256 combinations per sweep. Checked locally: a
licensed run times the grid, an unlicensed one is skipped, and
parity-only still runs below the limit and is skipped above it.
This commit is contained in:
Exocet92
2026-10-23 23:06:12 +02:00
committed by Jimmy7892
parent 5ac5c6c5b7
commit 556c6ceabd
5 changed files with 32 additions and 49 deletions
+3 -5
View File
@@ -242,11 +242,9 @@ jobs:
fi
pip install -r benchmarks/vs_vectorbt/requirements-lock.txt
# A sweep benchmark without a licence does not fail, it produces a wrong
# number: every unlicensed fan-out call waits a fixed interval before any
# work starts, so the stopwatch would time the wait. This step exits
# non-zero rather than let that happen, and the harness refuses again on
# its own if the tier is not what it expects.
# Sweep timings are published from a licensed run only. This step exits
# non-zero without a licence, and the harness refuses again on its own if
# the tier is not what it expects.
- name: Activate the benchmark licence
env:
MANIFOLDBT_CI_LICENSE: ${{ secrets.MANIFOLDBT_CI_LICENSE }}
+3 -4
View File
@@ -334,10 +334,9 @@ On the raptorbt side specifically:
## Parameter sweeps
Sweeps run in their own CI job, because they need a licence: an unlicensed
fan-out call waits out a fixed interval before doing any work, so a stopwatch
would be timing the wait. The harness refuses to produce a number in that state
rather than producing a wrong one.
Sweeps run in their own CI job, because they need a licence: sweep timings are
published from a licensed run only, and the harness refuses to produce a number
without one.
Three points, sized from what the runner actually did rather than guessed.
Measured there: 87.5 us per combination for manifoldbt at 20,000 bars, 1.16 ms
+2 -3
View File
@@ -600,9 +600,8 @@ def main() -> int:
parser.add_argument("--sweep", type=parse_point, nargs="*", default=[],
metavar="BARS:COMBOS",
help="parameter-grid points, e.g. 20000:5000. Needs a "
"licence: an unlicensed fan-out call waits out a "
"fixed interval, so the stopwatch would be timing "
"the wait rather than the engine")
"licence: sweep timings are published from a "
"licensed run only")
parser.add_argument("--sweep-reps", type=int, default=3,
help="repetitions per sweep point; fewer than --reps "
"because a large grid costs seconds, not milliseconds")
+4 -9
View File
@@ -1,12 +1,7 @@
"""Activate a licence from the environment, then assert the tier out loud.
Used by the sweep workflow. A sweep benchmark run without a licence does not
fail, it produces a wrong number: every unlicensed fan-out call waits out a
fixed interval before doing any work, so the stopwatch measures the wait. On a
100-cell grid that read 5.00 s against vectorbt's 0.17 s, which would publish
"vectorbt is 29x faster" from a run where the engine barely ran.
So this exits non-zero rather than let the benchmark continue unlicensed.
Used by the sweep workflow. Sweep timings are published from a licensed run
only, so this exits non-zero rather than let the benchmark continue without one.
The key is written to disk by `activate`, so every child process the harness
spawns afterwards picks it up without seeing the secret itself.
@@ -45,8 +40,8 @@ def main() -> int:
print(f"activation failed: {type(exc).__name__}")
return 1
_used, _limit, is_pro = mbt._native._combo_budget()
if not is_pro:
tier, _ = mbt.license_info()
if tier != "Pro":
print("activation did not yield a licensed tier: refusing to continue.")
return 1
print("licensed tier active")
+20 -28
View File
@@ -5,13 +5,11 @@ broadcasts the whole grid into one vectorised simulation. So it is the workload
where a speed claim needs the tightest gate, and where engines are the easiest
to compare *wrongly*.
Its own process, for three reasons:
Its own process, for two reasons:
* memory. The peak of a large grid is the number that decides what a machine can
run at all, and it cannot be read once another engine has already grown the
allocator in the same process.
* tier. The engine's fan-out allowance is counted per process, so two points
sharing one would interfere.
* isolation. A 100k-combination grid that runs out of memory takes its process
down with it; one point dying should not cost the whole benchmark.
@@ -106,18 +104,19 @@ def grid(combos: int) -> tuple[list[int], list[int]]:
return fast_vals, slow_vals
def tier() -> dict:
"""What the engine thinks it is allowed to do, right now.
# The Community limit on a single sweep, as published on the pricing page.
COMMUNITY_SWEEP_LIMIT = 256
Read before and after the sweep. The downgrade that follows a refused
licence ping lands from a background thread, so a run can legitimately start
Pro and finish Community: a number measured across that boundary is not a
measurement of anything.
def tier() -> dict:
"""The licence tier the engine reports, read before and after the sweep.
A number measured across a tier change is not a measurement of anything.
"""
import manifoldbt as mbt
used, limit, is_pro = mbt._native._combo_budget()
return {"pro": bool(is_pro), "budget_used": int(used), "budget_limit": int(limit)}
name, _ = mbt.license_info()
return {"pro": name == "Pro"}
def run_mbt(df, fast_vals, slow_vals, workdir, metrics=False):
@@ -507,13 +506,9 @@ def _main(args) -> int:
}))
return 0
# An unlicensed sweep CANNOT be timed, and this is not a matter of degree.
# Every accepted fan-out call waits out a fixed interval before any work
# starts, so the stopwatch measures the pause, not the engine: a 100-cell
# grid on 20k bars measured 5.00 s against vectorbt's 0.17 s here, which
# would publish "vectorbt is 29x faster" from a run where the engine did
# almost nothing. Timing is therefore refused outright without a licence,
# rather than gated on grid size.
# Sweep timings are published from a licensed run only, the configuration
# this benchmark describes. Timing is therefore refused outright without a
# licence, rather than gated on grid size.
#
# Checking tier_before against tier_after is not enough on its own: a run
# that starts AND finishes unlicensed shows no change at all, and would sail
@@ -524,24 +519,22 @@ def _main(args) -> int:
"combos": actual,
"status": "skipped",
"reason": (
"sweep timing requires a licence: unlicensed fan-out calls wait "
"out a fixed interval, so the measurement would be of that wait. "
"sweep timing requires a licence. "
"Re-run with --parity-only to check agreement without timing."
),
"tier_before": tier_before,
}))
return 2
# Parity-only still has to fit the unlicensed allowance, which is spent
# across the whole process: one warmup call per engine and nothing more.
if args.parity_only and not tier_before["pro"] and actual > tier_before["budget_limit"]:
# Parity-only still has to fit the Community limit on a sweep.
if args.parity_only and not tier_before["pro"] and actual > COMMUNITY_SWEEP_LIMIT:
print(json.dumps({
"bars": args.bars,
"combos": actual,
"status": "skipped",
"reason": (
f"{actual} combinations exceeds the unlicensed allowance of "
f"{tier_before['budget_limit']} for a single call"
f"{actual} combinations exceeds the Community limit of "
f"{COMMUNITY_SWEEP_LIMIT} per sweep"
),
"tier_before": tier_before,
}))
@@ -650,9 +643,8 @@ def _main(args) -> int:
result["memory_baseline_mb"] = round(base_mb, 1)
result["tier_after"] = tier_after
# A sweep measured across a tier change is not a measurement. The downgrade
# arrives from a background thread after a refused licence ping, so this can
# only be checked after the fact.
# A sweep measured across a tier change is not a measurement, and a change
# can only be seen after the fact.
if tier_before["pro"] != tier_after["pro"]:
result["timings"] = None
result["note"] = (