Frequently asked questions
Sourced from whest-starterkit @
96e308c.
Frequently asked questions
Is bit packing allowed if all operations are metered?
No. In Phase 2, packing independent values into a single machine element to gain an accounting advantage is not permitted. The Rules give the example of packing booleans into wider integers and operating on them with bitwise operations. Paying for the packing and unpacking does not change the policy.
The official Challenge Rules govern eligibility, including where earlier FlopScope documentation calls packing "in-bounds". Ordinary quantization or choosing a lower-precision dtype is not by itself this packing technique. See Fair accounting and packing for the distinction and how to ask about a specific approach.
Can I use numpy directly?
No. Plain import numpy is not available in the grader sandbox (by design). All array math goes through flopscope (import flopscope as flops and import flopscope.numpy as fnp), which wraps numpy with analytical FLOP counting. Your score depends on the FLOP cost of your operations, and only flopscope tracks those costs, so flopscope is both the only array path and the only one that counts.
Shipping your own copy is not a way around it. Vendored array libraries, compiled kernels of any kind, ctypes/cffi/FFI, and anything else that computes outside flopscope are prohibited: a submission that does it is disqualified, not merely charged for the time it took. Shipped data (weights, lookup tables, precomputed artifacts) remains entirely welcome; it is shipped code that computes that is not. The full boundary is in Allowed Code.
Can I use scipy (or PyTorch, or any other PyPI package)?
No. At grading time your estimator runs in a locked-down sandbox whose only importable libraries are flopscope (incl. flopscope.numpy as fnp), the whestbench API (BaseEstimator, MLP, SetupContext), and the Python standard library. There is no requirements.txt install step: third-party packages (scipy, numpy, torch, …) are not installed and won't import. For the standard normal CDF, use the pure-flopscope norm_cdf recipe in Code Patterns. For anything heavier, such as a model trained with PyTorch, do the work offline before packaging and ship the result as a pickle-free .npz, loaded in setup() via fnp.load(str(path)) (0 FLOPs); see Ship Weights. Bundling the package into your submission instead is prohibited and disqualifiable: ship the result, never the library.
Why is one MLP scoring much worse than the others?
A per-MLP adjusted_final_layer_score that is much higher than the others almost always means that MLP failed: your estimator raised, exceeded the FLOP budget, exceeded the wall-time cap, returned the wrong shape, or returned non-finite values. WhestBench treats every failure as if your estimator had returned a zero array and forces the per-MLP multiplier to 1.0 (no compute discount). Concretely: adjusted_final_layer_score_m = MSE(0, Y_m) × 1.0 for the failed MLP, which is strictly worse than a trivial-zero submission that succeeds (which gets the 0.1 multiplier floor).
The suite mean stays finite: one failed MLP no longer poisons the whole run, but it does pull the mean noticeably toward the raw final_layer_mse of the zero prediction (0.9095, measured across the @v2-phase2 Mini split).
Diagnose by reading the failure flags on the failing per-MLP entry: budget_exhausted, time_exhausted, residual_wall_time_exhausted, combined_budget_exhausted, error / error_code / traceback. The suite-level failure_breakdown gives counts per flag, and n_failed_mlps is the total count of MLPs that hit any failure path.
To see tracebacks, run with --debug; to halt at the first failure, add --fail-fast:
uv run whest run --estimator estimator.py --debug
uv run whest run --estimator estimator.py --debug --fail-fast # halt at first errorSee Estimator Contract: Failure semantics for the complete list of failure paths.
Do I need to use the budget argument in predict()?
No. flopscope enforces the budget whether you read the argument or not: if your
operations exceed it, BudgetExhaustedError is raised and your predictions are
zeroed.
The budget argument tells you how many FLOPs you are allowed. It's usually best
to use it as a fixed hard cap and stay with one strategy throughout the run.
Can I precompute things in setup()?
Yes, and the FLOPs really are free: setup() runs outside the per-MLP FLOP budget, and its wall time is not charged to residual_wall_time_s either.
The constraint is wall clock. setup() runs under a hard 5 s ceiling, and overrunning it fails the whole submission with SETUP_TIMEOUT — not one MLP, all of them.
And it is not a once-per-submission hook. The grader runs setup() once per worker process, a submission is served by several workers, and a worker that dies mid-suite is replaced and re-runs it — roughly 5-15 times per submission in practice. Each run gets its own 5 s, so anything expensive is paid for again on every worker. Keep it idempotent and cheap on every call. See Estimator Contract: Lifecycle.
That makes setup() the right place to load MLP-independent work (weights, lookup tables, configuration) and the wrong place to generate it. If the precompute is expensive, do it offline and ship the result (see Ship Weights).
I added a helper module or weights file, but it didn't end up in my submission.
whest package --estimator estimator.py (and whest submit --estimator estimator.py) ship only that one file. To ship more than one file, keep them in a folder and point --estimator at the folder:
uv run whest package --estimator . --output submission.tar.gzYou'll see the full list of files before anything is sent, and credential files like .env are never included.
Since whestbench 0.16.0 this cannot happen silently for a file you import: whest package prints ⚠ Single-file submission: only estimator.py will be submitted. N items beside it will NOT be included: …, naming each one, and it refuses with Error [package:PACKAGING_VALIDATION_ERROR] if your single file imports one of them. The fix is the same: point --estimator at the folder. The warning stays advisory for files your estimator does not import (a weights .npz opened by a computed path, say), so still read the list.
See Ship Weights and Multi-File Submissions.
How do I set a time limit on my estimator code?
At the flopscope level, time limits live on BudgetContext via
wall_time_limit_s:
import flopscope as flops
with flops.BudgetContext(flop_budget=10_000_000, wall_time_limit_s=2.0) as budget:
...In WhestBench CLI runs, --wall-time-limit sets that same limit for each
predict() call.
What is residual_wall_time_limit?
residual_wall_time_limit is a WhestBench rule, not a BudgetContext
parameter. flopscope reports:
flopscope_backend_time_s: time spent inside counted flopscope callsflopscope_overhead_time_s: time spent inside flopscope's own dispatchresidual_wall_time_s: participant Python (loops, control flow), GC, and Python-callback op time; as of flopscope 0.7.0, data-movement NumPy ops (concatenate, stack, tile, repeat, take, pad, …) count asflopscope_backend_time_s, not residual
In Phase 2 that limit is a hard cap of 400 ms per MLP, and it is a cap
rather than a price: residual wall time is not converted into FLOPs and charged
to your budget. Overrun it and WhestBench zeros your predictions for that MLP
and forces the multiplier to 1.0 (residual_wall_time_exhausted: true),
the same outcome as any other per-MLP failure.
The cap is sized for plumbing: control flow, slicing inputs into blocks, bookkeeping between flopscope calls. It is not an allowance for computation. Doing meaningful compute in residual time (raw Python arithmetic over neurons, a vendored library, a compiled kernel, a subprocess) is prohibited and disqualifiable, whether or not it happens to fit inside 400 ms (see Allowed Code).
Rehearse the cap locally by passing it explicitly:
uv run whest run --estimator estimator.py --residual-wall-time-limit 0.4If your estimator has a legitimate structural need for more residual time than the cap allows, don't engineer around it; write to arc-whestbench@aicrowd.com and describe the case before you submit.
What happens if I exceed the FLOP budget?
flopscope raises BudgetExhaustedError before the over-budget operation executes. The framework catches this, zeros all your predictions for that MLP, and forces the per-MLP multiplier to 1.0 (no compute discount). You will see budget_exhausted: true in the per-MLP report and adjusted_final_layer_score_m = final_layer_mse_m × 1.0 for the affected MLP. There is also a post-hoc combined-budget check: after predict() returns, the scoring layer re-checks effective compute against the budget and surfaces combined_budget_exhausted: true (same zero/×1.0 outcome). In Phase 2 effective cost is C_m = F_m (residual wall time is capped at 400 ms, not priced into it), so that check is a backstop on the same FLOP count; blowing the residual cap surfaces separately as residual_wall_time_exhausted.
Is there a memory limit?
Yes, two. Your solution process gets 8 GB of RAM in total, and the grading sandbox additionally caps any single array at 4 GiB, on both the arrays you build via flopscope.numpy and the array you return from predict() (at smoke and grading). The array cap is a memory-safety guard, not a scoring rule, and it sits far above what an efficient estimator needs. If you hit result array too large, chunk into row/column blocks; reshapes and allocations cost 0 FLOP, so it's free against your budget. (Local whest run has no such cap, so this only appears on the server.)
Is there a limit on total evaluation time?
Yes, two that can cut a running evaluation short, and they apply to the submission as a whole rather than to any one MLP. The grader finalizes your evaluation when no MLP has finished for 8 minutes, or when the evaluation has been running for 40 minutes in total, whichever comes first. Any MLP not finished by then is recorded as WATCHDOG_TIMEOUT and counts as a failed MLP (predictions zeroed, multiplier forced to 1.0); MLPs already scored keep their scores. These are grader-side operational limits, not whestbench settings; they can change without a kit release.
A third submission-level bound is not a watchdog at all: if the cluster never serves your session, it is finalized with truncation.reason = "no_worker_capacity" rather than no_progress. That one is a grader-side fault: nothing in your submission caused it, and there is no partial result to read. Re-submit.
There is no per-worker time budget. The 40 minutes is a single wall-clock ceiling on the whole submission, not a per-machine allowance that gets divided up. A working estimator does not come close: the 120 s per-MLP cap bounds each call and MLPs are graded in parallel. But an estimator that sits near the per-MLP cap on every MLP has no margin left. The limits are there to terminate a stuck evaluation, not to constrain a slow one. See Whole-submission timeout. (Local whest run has no equivalent timer, so this only appears on the server.)
How do I inspect budget summaries while debugging?
Use:
budget.summary()for the current explicitBudgetContextflops.budget_summary()for the accumulated process/session viewbudget.summary_dict(...)orflops.budget_summary_dict(...)for structured data
If you want namespace attribution, pass by_namespace=True.
Is scoring hardware-dependent?
Partly.
F_m, the analytical FLOP count, is not. flopscope derives it from tensor
shapes and dtypes, so it is identical on your laptop and on the grader.
R_m, the residual wall time, is. Phase 2 does not price it (effective
compute is C_m = F_m), but it does cap it, at 400 ms per MLP, and
overrunning the cap zeroes that MLP. Everything flopscope does not meter
(pure-Python loops, control flow, GC, I/O) runs at the speed of whatever machine
the grader hands you, so a loop sitting at 300 ms on your laptop can trip a cap
you thought you were under. Moving real computation into that window is not an
option either: raw numpy, compiled extensions you ship yourself, threads,
subprocesses, and multiprocessing are prohibited in Phase 2, and using them
is disqualifiable rather than merely expensive.
The rules do not guarantee any particular evaluation hardware. Do not assume the grader matches your development machine in core count, clock speed, or BLAS backend. A submission whose cost or behavior depends on a fixed amount of wall clock is carrying a risk it does not need to carry.
The way to stay hardware-independent is to keep the work inside
flopscope.numpy, where it is metered analytically and your local number is
the grader's number. Anything you move outside it is timed instead of metered.
If your estimator has an internal deadline (time.time() checks, a
PREDICT_TIMEOUT constant, an anytime algorithm that stops refining when the
clock runs out), do not calibrate it against your own machine's speed. On
slower hardware a locally-tuned deadline trips early and your estimator
silently returns its fallback answer: no exception, no failed MLP, only a
valid-looking score computed from the degraded path. Prefer a budget-based
cutoff over a clock-based one: predict() is handed budget, and
flops.budget_summary() tells you what you have spent. If you must use a clock, make the
fallback something you would be content to be scored on.
To see where you stand, run with --profile and read residual_wall_time_s
against the 400 ms cap. A conservative local check is --max-threads 1, which
takes away the BLAS parallelism you cannot count on having.
How many MLP networks are in a full evaluation?
Two different numbers, so be careful which one you are sizing against:
- Locally,
whest rundefaults to 10 MLPs (configured byn_mlpsinContestSpec). Raise it with--n-mlpsif you want a closer rehearsal. - On the grader, a full evaluation scores 100 MLPs, split into a public half that drives the visible leaderboard and a held-out half.
Each MLP has the same width and depth but different random weights and a distinct grader-supplied mlp.seed for any estimator-side randomness. Your aggregate score is the mean of the per-MLP adjusted_final_layer_score values.
The FLOP budget is per MLP, not shared across the run, so scoring 100 instead of 10 does not shrink the budget available to any one of them. What it does change is total time: see Is there a limit on total evaluation time?.
What if my estimator is fast but inaccurate?
You are ranked by the budget-adjusted adjusted_final_layer_score = final_layer_mse × max(0.1, C_m / B), not raw MSE. Using less than 10% of the effective-compute budget gets you the 0.1 multiplier floor, a factor-of-ten discount and no more. So extremely cheap and inaccurate beats moderately cheap and inaccurate only up to that floor; below it, there is no further benefit to being cheaper.
My local score is great but my submission scores 10x worse — why?
Almost always one of four things:
-
State carries between MLPs locally, but the grader spreads your suite over several workers. Module-level and instance state survives from one
predict()to the next in both local runners: one worker serves the whole local suite,setup()runs once, and--runner subprocessbehaves exactly like--runner localhere. The grader does not. It serves one submission from several worker processes, sosetup()runs roughly 5-15 times and each worker sees only the MLPs it was handed; a worker that dies mid-suite is replaced and starts over. A cache that assumes it has seen every previous MLP is therefore populated locally and empty in production. Fix: make eachpredict()correct on its own, and treat anything built across calls as an optimization that may be missing. Populate what you can insetup()fromself._...attributes, and ship precomputed data as a file loaded fromsubmission_dir(Ship Weights). Do not plan caching aroundSetupContext.scratch_dir: it is reserved andNoneon every whestbench 0.16.0 path, so touching it raises insidesetup(), which fails the entire submission rather than one MLP. -
Imports that work locally fail in the grader sandbox. Two flavors: (a) a helper module that didn't ship (you packaged the single file instead of the folder) or a side-effecting top-level statement, caught by running
uv run whest run --estimator estimator.py --runner subprocesslocally before submitting, then reading the "Estimator Errors" panel; (b) animportof a package the grader doesn't provide. The sandbox has onlyflopscope, thewhestbenchAPI, and the Python stdlib: nonumpy/scipy/torch. Your local venv does have those, so a local run won't flag them; the fix is to not import them (useflopscope.numpy as fnp, and precompute heavier work offline; see Ship Weights). -
Numerical non-determinism without a seed. Random MLP generation, Monte-Carlo ground truth, or your estimator's own RNG. Fix: add
--seed Nto your local runs so successive runs are comparable, and avoid time-based seeds in your estimator. -
Work that flopscope never metered. Raw
numpy, a bundled BLAS, threads, andmultiprocessingare prohibited in Phase 2: a submission that computes outside flopscope is disqualified, not re-priced. Even legitimate plumbing (Python loops, control flow, GC, I/O) runs at whatever speed the grader's machine gives it, andresidual_wall_time_scan be several times larger there than at home. Symptom:flops_usedmatches your local run exactly, whileresidual_wall_time_exhaustedfires on MLPs that passed locally. A clock-based internal deadline can also trip early and silently substitute a fallback answer. Fix: move the math intoflopscope.numpyso it is metered analytically, and rehearse with--residual-wall-time-limit 0.4 --max-threads 1. See Is scoring hardware-dependent?.
If your Stage 3 and Stage 4 scores agree but the grader still disagrees, suspect Python-version or BLAS-version drift; uv run whest doctor will surface the relevant runtime info.
How many submissions can I make per day?
Ten per team per UTC day. The counter resets at 00:00 UTC, and a submission that fails on the grader still spends one, so rehearse at Stage 4 before you spend a slot.
Who do I contact about a rules question or a FLOP that looks mispriced?
Email arc-whestbench@aicrowd.com. Use it for:
- rules questions — whether the thing you want to build is allowed, before you build it;
- suspected FLOP mispricing — an operation whose billed cost doesn't match
what it should be. Send a minimal repro: the op, the shapes and dtypes, and
the
ctx.summary()rows you're reading it from; - a legitimate need for more residual wall time than the 400 ms cap allows — describe the structural reason, before you submit rather than after.
Anything that can be discussed in the open — scoring questions, "why is my MSE like this", packaging trouble — moves faster on the challenge discussion forum.
Did the scoring formula change when the kit updated?
No. adjusted_final_layer_score = final_layer_mse × max(0.1, C_m / B) is
unchanged. What changes between kit releases is the meter, and it has moved
several times. whestbench 0.13.0 adopted flopscope 0.9 (float64 2×,
transcendentals 16×, formerly-free data movement now billed). whestbench 0.14.0
adopted flopscope 0.10: out= destinations priced, symmetry-tag discounts voided
when the tagged buffer is written (re-validate with as_symmetric() to keep them,
as examples/03_covariance_propagation.py shows), and einsum(..., out=) following
NumPy's casting rules so calls that used to silently truncate now raise
TypeError. whestbench 0.15.0 adopted flopscope 0.11: a contraction past the
52-letter subscript budget now bills the full fused-multiply-add count instead of
multiplies only, ix_ moved from free to billed, and non-numeric dtypes
(object, str_, bytes_, datetime64, structured) now raise
UnsupportedDtypeError anywhere they reach a metered op. Convert those with plain
numpy before handing data to flopscope.
Whestbench 0.16.0 adopted flopscope 0.12.0, with roughly forty operations repriced
in both directions. Costing more: nan* reductions now pay for the NaN-scan pass
they always ran (integer and boolean input exempt, and nanmax/nanmin exempt),
six boolean-answering ops that compute in a promoted float dtype (signbit,
isneginf, isposinf, isclose, allclose, and is_symmetric/as_symmetric)
stop undercharging by 2×, a narrow out= can no longer lower a unary op's price,
and angle on boolean input doubles. Costing less: where/insert mixed with a
Python scalar follow NEP 50 weak promotion, a refused percentile/quantile
costs nothing, ldexp is priced on its mantissa loop, and a dtype combination
NumPy cannot run is no longer billed before it raises. flopscope ships 0.12.0 as
a new version opening a new phase, so nothing already scored was repriced.
Update the meter before comparing local results with the leaderboard.
The kit now locks flopscope 0.12.3 with whestbench 0.16.2, the latest
published harness. Its explicit flopscope>=0.12.3,<0.13.0 dependency prevents
uv sync from retaining the older 0.12.1 meter.
Flopscope 0.12.2 corrects broadcast and dtype billing, histogram/FFT/covariance
charges, and symmetry tracking. Version 0.12.3 adds rank-dependent work to
truncated SVD. Calls affected by these fixes can cost more; check the
0.12.2 release notes
and 0.12.3 release notes
for the affected paths. The bundled examples' documented FLOP totals remain
covered by tests/test_flopscope_cost_docs.py.
Whenever the meter moves, F_m (and therefore C_m and the multiplier) can
change for identical code. Leaderboard scores are re-evaluated under the new
meter; local scores taken under an older kit are not comparable. Pull the latest starter kit, run
uv sync, and re-measure rather than trusting a number you recorded earlier. If
the number you are comparing against predates Phase 2, the shape and the cost
model moved too. Competition Rounds lays every round
side by side and gives the flags that reproduce an earlier one.