feat: tell the job how large its claim is - #4
Open
stephenc wants to merge 4 commits into
Open
Conversation
A claim controls the queue, and it does not control the job. A job that
asks the machine how many cores it has receives the number of the
MACHINE, so a job with a claim of two cores on a machine of sixteen
starts sixteen threads. It then takes the capacity that qex gave to the
other jobs, and the promise of the queue is broken by the job that made
it.
Most runtimes read a variable in place of the machine, so qex now writes
those variables from the resolved claim:
$ qex submit --cpu 2 --mem 2GB -- go run main.go
go: NumCPU=16 GOMAXPROCS=2
GOMAXPROCS, GOMEMLIMIT, OMP_NUM_THREADS, OPENBLAS_NUM_THREADS,
MKL_NUM_THREADS, NUMEXPR_NUM_THREADS, VECLIB_MAXIMUM_THREADS,
RAYON_NUM_THREADS, CARGO_BUILD_JOBS, JULIA_NUM_THREADS,
DOTNET_PROCESSOR_COUNT, POLARS_MAX_THREADS and NODE_OPTIONS. And
QEX_CPU, QEX_MEM and QEX_MEM_MB, so a script needs no other tool:
`make -j"$QEX_CPU"`.
This is the nearest thing to a limit that operates on macOS as well as on
Linux, and it needs no cgroup and no privilege. It stays a promise: a
program that asks the operating system directly still sees the whole
machine.
QEX NEVER REPLACES A VALUE THAT IS ALREADY THERE. A value from the shell,
from the job file or from `--env` is a decision that somebody made, and
this code fills the values that nobody chose.
`--env-capture none` receives nothing either. That mode says that the job
starts with an empty environment and receives `[env]` and `--env` ONLY,
and sixteen variables that qex chose would break that promise. A test
that already existed caught this, and it now holds the decision.
`--no-limit-env-hints` turns it off for one job, and
`[claims] export_env = false` for every job.
MEASURED, AND NOT ASSUMED. Three runtimes were installed and read their
own values, and two results changed the list:
-XX:MaxRAMPercentage does NOT limit the heap to the claim, because the
JVM takes that percentage of the memory of the MACHINE: a claim of 2GB
on a machine of 28GB still gave a heap of 7GB. A limit needs -Xmx.
node gives no variable for the number of cores. availableParallelism()
stays at the number of the machine.
JAVA_TOOL_OPTIONS and MAKEFLAGS need `[claims] also = ["java", "make"]`,
because each has a cost: every JVM writes `Picked up JAVA_TOOL_OPTIONS:`
to its standard error and that line lands in the log of the job, and
MAKEFLAGS replaces the -j of a Makefile that gives one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNvagiMEU3myn8EXGaGEM9
`--no-limit-env-hints` existed on the command line only. A job file that
gave the same name was REFUSED, because a job file rejects a field that
qex does not know — so the option could not be written down beside the
command that needs it.
The field now exists on a job file and on a pipeline stage, and the order
is the order of every other value: the command line replaces the file,
and the file replaces the configuration.
no_limit_env_hints = true
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNvagiMEU3myn8EXGaGEM9
An independent review of this branch found that it would break the jobs of every user who had not given a claim, and it was right. THE DEFAULT CLAIM IS ONE CORE. This branch wrote that number into the environment, so `qex submit -- cargo test` on a machine of sixteen cores became `GOMAXPROCS=1` and `CARGO_BUILD_JOBS=1`: sixteen times slower, with no error and no warning. A node job met something worse. The default memory claim is the memory of the machine divided by its cores, which gave a heap far below the heap that node takes by itself, and the job stopped with `FATAL ERROR: Reached heap limit`. The review made both faults happen. The learn loop then made the fault permanent: qex measured the job that it had capped at one core, learned one core, and capped it again on the next run. The rule of this feature is that A VALUE THAT SOMEBODY CHOSE IS A DECISION. A claim that qex invented is not such a decision, and neither is a claim that qex learned. qex now writes these variables only when the claim came from the person: `--cpu` and `--mem`, a job file, or a pipeline stage. THREE MORE FROM THE SAME REVIEW. `[claims] export_env = false` did not turn the feature off. A job file that said `no_limit_env_hints = false` — the line a user writes believing it repeats the default — turned it on again. The configuration now comes first. `-Xmx0m` makes the JVM refuse to start, and qex accepts a claim small enough to give that number. A claim below one megabyte of heap now gives the count of the cores and no heap. The pages said that a value from the shell always wins. That is true for the whole environment and it is false for `--env-capture minimal`, where the value does not survive the capture. The pages now say which. The version moves to 0.8.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KNvagiMEU3myn8EXGaGEM9
`source` said "explicit" when ONE half was explicit, so `--mem 4GB` with no `--cpu` gave the job GOMAXPROCS=1 on a machine of 16 cores. Measured before the change: `qex submit --mem 4GB -- sh -c 'echo $GOMAXPROCS'` printed `1`. Two tests passed for the wrong reason and now hold the rule. Add the coverage that the mutation test found missing: the config file, a pipeline stage, the whole list of variables, and a claim below one megabyte.
stephenc
force-pushed
the
feat/claim-env
branch
from
August 7, 2026 20:05
19c9016 to
4e3026a
Compare
Owner
Author
|
Pre-merge checklist
|
There was a problem hiding this comment.
Pull request overview
This PR ensures jobs are informed of their resolved CPU/memory claim via environment variables so language runtimes and build tools size their thread pools and heaps to the claimed share rather than the full machine.
Changes:
- Export claim-derived env vars (e.g.,
QEX_CPU,QEX_MEM(_MB),GOMAXPROCS,GOMEMLIMIT,OMP_NUM_THREADS,NODE_OPTIONS, etc.) only when both--cpuand--memwere explicitly provided (and not when--env-capture noneis used). - Add opt-out controls via CLI (
--no-limit-env-hints), job files / pipeline stages (no_limit_env_hints = true), and global config ([claims] export_env = false, plus[claims] also = ["java","make"]). - Add/extend unit + e2e tests and update help/reference documentation to describe the behavior and configuration.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/e2e.rs | Adds end-to-end coverage for when claim env vars are exported, preserved, or suppressed across CLI/job-file/pipeline/config scenarios. |
| src/spec.rs | Implements claim env export during job resolution, adds the no_limit_env_hints controls, and introduces export_claim with targeted runtime env vars. |
| src/pipeline.rs | Plumbs no_limit_env_hints from pipeline stages into the generated JobFile spec. |
| src/help.rs | Documents no_limit_env_hints in job-file help output and introduces [claims] config fields in config help. |
| src/config.rs | Introduces [claims] configuration (export_env, also) and wires it into the global Config. |
| src/commands.rs | Passes the new no_limit_env_hints option through submit/run command resolution. |
| src/cli.rs | Adds the --no-limit-env-hints CLI flag with user-facing help text. |
| README.md | Updates the README to describe that claims are exported to job environments and never override user-provided values. |
| docs/reference.md | Adds a reference section documenting exported variables, the “both cpu+mem” rule, and all opt-out / opt-in controls. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+642
to
+646
| // This replaces the `-j` of a Makefile that gives one. | ||
| set("MAKEFLAGS", &format!("-j{cores}")); | ||
| } | ||
| _ => {} | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A claim controls the queue, and it does not control the job. A job that asks
the machine how many cores it has receives the number of the MACHINE, so a job
with a claim of 2 cores on a machine of 16 starts 16 threads. It then takes the
capacity that qex gave to the other jobs, and it breaks the promise that it
made.
Most runtimes read a variable in place of the machine. qex now writes those
variables from the resolved claim:
An agent that queues a build, a test suite or a training run thus receives the
share that it asked for, and the other users of the machine keep theirs.
QEX_CPU,QEX_MEMandQEX_MEM_MBhold the claim for your own script, so itneeds no other tool:
make -j"$QEX_CPU". The runtimes receiveGOMAXPROCS,GOMEMLIMIT,OMP_NUM_THREADS,OPENBLAS_NUM_THREADS,MKL_NUM_THREADS,NUMEXPR_NUM_THREADS,VECLIB_MAXIMUM_THREADS,RAYON_NUM_THREADS,CARGO_BUILD_JOBS,JULIA_NUM_THREADS,DOTNET_PROCESSOR_COUNT,POLARS_MAX_THREADS, andNODE_OPTIONSwith a heap of three quarters of theclaim.
This is the nearest thing to a limit that operates on macOS as well as on
Linux, and it needs no cgroup and no privilege. It stays a promise: a program
that asks the operating system directly still sees the whole machine.
The change of behaviour
A job that gives BOTH
--cpuand--memreceives these variables. Every otherjob receives none of them:
job that heard that number would run single-threaded on a large machine.
it made single-threaded, learn one core, and make the fault permanent.
from the defaults or from the learning, and that half is not a decision.
qex never replaces a value that is already there. A value from your shell,
from the job file or from
--envis a decision that somebody made, and qexfills the values that nobody chose.
--env-capture nonereceives no claim either. That mode promises[env]and--envonly, and none means none.How to turn it off
Use
qex submit --no-limit-env-hintsfor one job. Writeno_limit_env_hints = truein a job file or in a stage of a pipeline. Write[claims] export_env = falsefor every job of the machine.Two variables go the other direction:
[claims] also = ["java", "make"]addsJAVA_TOOL_OPTIONSandMAKEFLAGS. Each of the two has a cost, so neither isa default. Every JVM writes
Picked up JAVA_TOOL_OPTIONS: ...to its standarderror, and that line lands in the log of the job.
MAKEFLAGSreplaces the-jof a Makefile that gives one.
A limit for the JVM needs
-Xmx, and not-XX:MaxRAMPercentage: the JVM takesthat percentage of the memory of the MACHINE, so a claim of 2GB on a machine of
28GB still gave a heap of 7GB. node has no variable for the number of cores, so
node receives the memory only.