Modernise jax-development: current provenance, donation-safe benchmarks, and private diagnostics (#19)

* Modernise JAX baseline and fix donated-buffer benchmarking and diagnostic disclosure

* Keep JAX metadata failures structured and retain private NVIDIA diagnostics
This commit is contained in:
Tristan Manchester
2026-09-14 09:36:06 +02:00
committed by GitHub
parent 5ee544d7b1
commit e1aa9a63f4
8 changed files with 387 additions and 975 deletions
+88 -210
View File
@@ -1,228 +1,106 @@
---
name: jax-development
description: Use this skill when the user is writing, debugging, profiling, refactoring, reviewing, benchmarking, parallelising, exporting, or explaining JAX code, or when they mention JAX, jax.numpy, jit, grad, value_and_grad, vmap, scan, lax, random keys, pytrees, jax.Array, sharding, Mesh, PartitionSpec, NamedSharding, pmap, shard_map, Pallas, XLA, StableHLO, checkify, profiler, or the JAX repo. It helps turn NumPy or PyTorch-style code into pure functional JAX, fix tracer/control-flow/shape/PRNG bugs, remove recompiles and host-device syncs, choose transforms and sharding strategies, inspect jaxpr/lowering/IR, and benchmark compiled code correctly.
compatibility: Best with Python 3.10+ and a working JAX installation. All bundled scripts are non-interactive and standard-library-only; they become richer when `jax` and `jaxlib` are importable or when a local JAX checkout is available.
description: >-
Write, debug, review, profile, or shard JAX numerical code. Use when the hard
part is JAX tracing, autodiff, control flow, PRNGs, compilation, array placement,
or runtime performance. Do not impose JAX on a NumPy-only or unrelated GPU task.
compatibility: Current-release baseline JAX 0.11.1 requires Python >=3.12; wheel/backend support must be checked separately. Diagnostics need Python 3.12+ and the project's JAX environment for runtime probes. Static helpers can run without JAX. Benchmark/probe commands execute trusted project code.
metadata:
author: OpenAI
version: "2.0.0"
category: scientific-computing
source_snapshot: "jax-main.zip from 2026-03-24"
version: "3.0.0"
reviewed: "2026-09-13"
source: "https://github.com/jax-ml/jax/tree/jax-v0.11.1"
---
# JAX Development
# JAX development
Use this skill for substantial JAX work. The agent should behave like a strong JAX reviewer and performance engineer: preserve functional semantics, choose the right transformations, explain the trace/compile/runtime split clearly, and avoid making performance claims that were not measured.
Make the mathematical contract explicit before changing transformations or kernels.
Record shapes, dtypes, numerical tolerances, randomness, differentiability, and
required backend. Preserve useful existing architecture; a new API does not itself
justify a rewrite. Use current public interfaces rather than compatibility shims.
This version is designed to be unusually agent-friendly. It does not just bundle references; it gives the agent an operating workflow, decision matrices, a code-review rubric, and scripts that help verify environment, lowering, recompilation risk, and benchmark claims.
## Inspect, reproduce, measure
## Core promise
Resolve `SKILL_DIR` to the directory containing this file. Bundled scripts live
there, not in the target project's `scripts/`. Run them in the project's actual
Python environment. Inspect each script's help before use.
When this skill is active, the default standard is:
1. produce runnable JAX code, not generic advice
2. explain why the change works in JAX terms
3. call out likely sharp bits even if the user did not ask
4. verify claims with the bundled scripts when possible
5. separate compile-time, run-time, transfer, and sharding issues instead of mixing them together
## When this skill should own the task
Use this skill when the difficult part of the request is any of the following:
- translating NumPy, SciPy, TensorFlow, or PyTorch code into idiomatic JAX
- fixing tracer, control-flow, PRNG, shape, dtype, or side-effect bugs
- choosing between `jit`, `vmap`, `scan`, `fori_loop`, `while_loop`, `cond`, `grad`, `jacrev`, `jacfwd`, `remat`, `shard_map`, or export
- removing recompiles, host-device round trips, Python overhead, or dishonest benchmarking
- reasoning about `jax.Array`, meshes, `PartitionSpec`, `NamedSharding`, explicit sharding, `pmap` migration, multi-host semantics, or collectives
- using `jax.debug.print`, `checkify`, `make_jaxpr`, lowering, compiler IR, profiler traces, or memory profiling
- using custom derivatives, export, AOT lowering, custom partitioning, Pallas, or the JAX source tree
Compose this skill with framework-specific skills when needed, but let this one own the JAX-specific reasoning.
## Do not over-apply the skill
Do not force JAX when the real problem is one of these instead:
- pure NumPy optimisation where JAX is explicitly out of scope
- generic CUDA, Triton, NCCL, or driver debugging with no meaningful JAX component
- framework-only design questions whose hard part is not JAX
- irregular dynamic object-heavy Python where the right answer is probably to keep the hot path outside JAX
When in doubt, ask: “Is the root of the problem tracing, transformations, array semantics, compilation, sharding, or the JAX runtime?” If yes, use this skill.
## First-response workflow
### 1. Classify the task
Put the request into one or more lanes immediately:
- code design or porting
- debugging or correctness
- performance or compilation
- sharding or distributed execution
- advanced extension points
- JAX repo navigation or source-level questions
Then open the matching reference file:
- `references/EXPERT-WORKFLOW.md` for the overall workflow
- `references/MENTAL-MODEL.md` for tracing and staging semantics
- `references/TRANSFORM-DECISION-MATRIX.md` for choosing primitives
- `references/PORTING-PATTERNS.md` for NumPy or PyTorch rewrites
- `references/CODE-REVIEW-RUBRIC.md` for self-review before replying
- `references/DEBUGGING-TRIAGE.md` for error diagnosis
- `references/PERFORMANCE-PLAYBOOK.md` for speed, memory, and compile-time work
- `references/SHARDING-PLAYBOOK.md` for distributed and multi-device design
- `references/ADVANCED-EXTENSIONS.md` for custom autodiff, export, Pallas, FFI, and internals
- `references/REPO-MAP.md` for local source-tree navigation
- `references/SOURCES.md` for provenance and maintenance notes
### 2. Inspect before guessing
If the problem could be environment-, backend-, or project-specific, inspect first.
Environment:
```bash
python3 scripts/jax_env_report.py --format json
python "$SKILL_DIR/scripts/jax_env_report.py" --format json
python "$SKILL_DIR/scripts/jax_project_scan.py" /absolute/project --format json
python "$SKILL_DIR/scripts/jax_compile_probe.py" --help
python "$SKILL_DIR/scripts/jax_recompile_explorer.py" --help
```
Static project scan:
Environment probing initialises the backend and can allocate resources. The
report lists relevant environment variable names, never their values, and returns
nonzero for import/backend/smoke-test failure. It is not an exhaustive secret
scanner: review paths, devices, and private project output before sharing.
Static scans produce leads, not proof that a transformation is wrong. Importing,
tracing, lowering, and benchmarking a module execute code; use trusted inputs.
Reduce failures to the smallest reproducer retaining the relevant shape, dtype,
static argument, transform order, and backend. Compare against a simple reference
and check numerical/gradient invariants before measuring speed. Change one
hypothesis at a time and record the observed result.
## Select the relevant reference
- Tracing, shapes, pytrees: [mental model](references/MENTAL-MODEL.md),
[transform decisions](references/TRANSFORM-DECISION-MATRIX.md), and
[porting patterns](references/PORTING-PATTERNS.md).
- Correctness: [debugging](references/DEBUGGING-TRIAGE.md) and
[review rubric](references/CODE-REVIEW-RUBRIC.md).
- Timing, compilation, memory: [performance](references/PERFORMANCE-PLAYBOOK.md).
- Placement, collectives, multi-host: [sharding](references/SHARDING-PLAYBOOK.md).
- Custom derivatives, export, Pallas, FFI: [extensions](references/ADVANCED-EXTENSIONS.md).
- Source-level diagnosis: [repo map](references/REPO-MAP.md),
[workflow](references/EXPERT-WORKFLOW.md), and [source baseline](references/SOURCES.md).
The source baseline records current changes that affect these longer references.
Check the installed release rather than treating online `latest`/unreleased docs
as the API of a locked project. Useful templates and reproducer/evaluation assets
remain bundled; run selected examples against the target environment.
## Correctness and performance rules
Keep ordinary transformed functions pure; explicit `jax.ref` state is a separate
supported model with its own transform/effect restrictions, not permission for
hidden Python mutation. Thread typed PRNG keys and avoid reuse. Use structured
control flow for traced decisions; static Python loops can be appropriate when
small, so do not mechanically replace every loop with `scan`.
Keep host transfers and synchronisation deliberate. Separate input preparation,
trace/compile, dispatch, device execution, output materialisation, and communication.
Check x64 and matmul precision explicitly. `jax.numpy.empty` no longer promises
zero-initialised storage on the current release; use zeros when zeros are needed.
Donation consumes input buffers. For independent timing trials, construct fresh
inputs for every call; never reuse a donated warm-up argument. The benchmark
harness now requires an input factory and propagates synchronisation errors:
```bash
python3 scripts/jax_project_scan.py PATH --format json
python "$SKILL_DIR/scripts/jax_benchmark_harness.py" \
--file /absolute/project/benchmark_case.py --function step --factory make_inputs \
--jit --donate-argnums 0 --repeat 20
```
Benchmark a callable honestly:
```bash
python3 scripts/jax_benchmark_harness.py --help
```
`make_inputs()` returns `(args, kwargs)` with equivalent, fresh inputs. Preparation
and its synchronisation are outside the timer; output synchronisation is inside.
Keep shapes/dtypes/shardings/static values fixed and verify outputs separately.
First-call time includes compilation/execution and may use caches; it is not pure
compile time. For stateful training throughput, write a separate benchmark carrying
returned state forward, and report that different workload. No old JSON/arrayify
or unsafe donation compatibility path remains.
Inspect jaxpr, lowering, and IR:
```bash
python3 scripts/jax_compile_probe.py --help
```
Prefer global-view code with deliberate sharding before manual `shard_map` when
it meets the objective. `NamedSharding` placement is not synonymous with explicit
sharding-in-types. Current mesh context uses `jax.set_mesh(mesh)`, not `with mesh`.
Test global/local shapes, replication, collectives, gradients, and output sharding;
a one-device run cannot validate multi-host communication.
Check likely recompile behaviour across cases:
```bash
python3 scripts/jax_recompile_explorer.py --help
```
## Finish with evidence
Search a local JAX checkout:
```bash
python3 scripts/jax_repo_locator.py --help
```
### 3. Reduce to a minimal reproducer
Prefer the smallest function that still exhibits the behaviour. JAX problems get much easier once shapes, dtypes, batching axes, randomness, and transformation boundaries are explicit.
### 4. Choose the least powerful mechanism that solves the problem
Default ordering:
- pure eager `jax.numpy` first
- then `jit` or `value_and_grad`
- then `vmap` or `scan`
- then explicit sharding
- then `shard_map`
- then custom derivative, export, custom partitioning, or Pallas
- then FFI or JAX internals
Escalate only with evidence.
### 5. End with a high-signal answer
Unless the user asked for something else, the reply should end with:
- diagnosis or design choice
- corrected code or patch
- why it works in JAX terms
- how to verify it
- remaining risks, backend caveats, or performance unknowns
## Expert operating rules
1. **Treat JAX functions as pure.** Inputs in, outputs out. Hidden mutation, global state, or implicit randomness are usually design bugs once transforms enter the picture.
2. **Make randomness explicit.** Thread keys through the program, split once per consumer, and return updated keys when state continues.
3. **Keep the hot path in JAX space.** Host conversion inside transformed code is almost always a bug or a sync point.
4. **Separate static and dynamic values.** Shapes, dtypes, Python objects, and some configuration values influence tracing and compilation.
5. **Use structured control flow.** If a branch or loop depends on array values, use JAX control-flow primitives instead of Python.
6. **Benchmark honestly.** Warm up, block, and distinguish transfer cost, compile cost, and steady-state execution.
7. **Optimise after evidence.** Use scans, compile probes, profiler traces, or lowering inspection before proposing deep rewrites.
8. **Prefer current JAX idioms.** Typed keys, `jax.Array`, and modern sharding APIs are the default unless the codebase is intentionally legacy.
9. **Think globally for sharding first.** Start with global-view code and explicit placement before dropping to per-device manual code.
10. **Never bluff backend-specific behaviour.** CPU, GPU, TPU, and multi-host runs differ materially. Say what was verified and what was inferred.
## Default red flags to proactively check
Always scan for these, even if the user did not mention them:
- `np.asarray`, `.item()`, `.tolist()`, `jax.device_get`, or printing arrays in a hot path
- Python `if`, `for`, or `while` inside transformed code
- shape construction or indexing based on traced values
- global or reused PRNG keys
- repeated creation of jitted callables inside loops
- changing shapes, dtypes, or static arguments causing compile storms
- very large Python loops that should be `scan` or `fori_loop`
- `pmap` code that may be better expressed with modern sharding APIs
- unexplained precision assumptions or implicit `x64` expectations
- replicated-versus-sharded confusion in distributed code
## Available scripts
- `scripts/jax_env_report.py` — report versions, backend, devices, config, env vars, and an optional smoke test.
- `scripts/jax_project_scan.py` — AST-based scan for common JAX sharp bits and migration targets.
- `scripts/jax_benchmark_harness.py` — benchmark a callable with warm-up, blocking, optional `jit`, and optional donation.
- `scripts/jax_compile_probe.py` — inspect `eval_shape`, jaxpr, lowering, and compiler IR; optionally write artefacts to disk.
- `scripts/jax_recompile_explorer.py` — run several input cases through a jitted function and flag likely recompiles or signature drift.
- `scripts/jax_repo_locator.py` — search a local JAX checkout for relevant docs, tests, or source files by topic.
All scripts are non-interactive, support `--help`, and default to structured JSON output.
## Available assets
- `assets/mre_template.py` — minimal reproducible example template
- `assets/training_step_template.py` — idiomatic compiled training step with explicit key plumbing
- `assets/scan_template.py` — carry-state loop using `lax.scan`
- `assets/sharding_template.py` — mesh plus `NamedSharding` starter
- `assets/shard_map_template.py` — manual SPMD starter using `jax.shard_map`
- `assets/benchmark_template.py` — honest timing pattern with warm-up and blocking
- `assets/profile_template.py` — trace and memory-profile starter
- `assets/checkify_template.py` — runtime checks that survive `jit`
- `assets/custom_vjp_template.py` — custom reverse-mode rule starter
- `assets/export_template.py` — export and serialisation starter
- `assets/pallas_kernel_skeleton.py` — kernel-level starting point
- `assets/issue_report_template.md` — compact bug report / investigation template
## Output quality bar
Before sending a final answer, mentally run the code or design through `references/CODE-REVIEW-RUBRIC.md`. The answer should usually satisfy all of the following:
- runnable or patch-ready code
- correct transformation and sharding semantics
- explicit discussion of compile and runtime consequences
- no accidental host round trips in the claimed hot path
- no hidden PRNG or state bugs
- an honest verification method
## If the task is exploratory research code
Prefer a staged plan:
1. get a correct eager version in `jax.numpy`
2. add tests or invariants
3. add transformations one at a time
4. benchmark and profile
5. only then attempt aggressive sharding or kernel work
This workflow beats premature `jit`/`pmap`/Pallas every time.
## Skill maintenance
When updating this skill, refresh the JAX facts most likely to drift:
- installation guidance
- sharding APIs and `pmap` migration status
- randomness recommendations
- profiler and memory-tooling guidance
- export / AOT APIs
- Pallas and custom extension interfaces
Return the diagnosis, patch/example, correctness checks, measured timings with
hardware and workload, and remaining backend limitations. Do not claim benchmarks
or compilation succeeded when a helper returned partial/error output. For helper
regressions run `python -m unittest discover -s "$SKILL_DIR/tests" -v`.
@@ -1,168 +1,66 @@
# Performance playbook
Use this file for compile-time blowups, slow steady-state execution, hidden synchronisation, memory pressure, or distributed performance work.
## Separate the costs
## First rule: separate the costs
State the workload and reference implementation, then distinguish host preparation,
transfer, trace/lowering/compilation, dispatch, execution, materialisation, and
communication. Validate numerical outputs and gradients before optimising. Record
backend/devices, versions, shapes, dtypes, precision, sharding, and cache state.
Never talk about “JAX performance” as one number. Separate:
## Benchmark independent inputs
- host-to-device transfer
- first-call trace and compile
- steady-state device execution
- synchronisation / materialisation
- communication or resharding
- memory pressure / OOM behaviour
The bundled `scripts/jax_benchmark_harness.py` imports a trusted module exposing
a function and a zero-argument input factory. Example target module:
Most bad optimisation advice comes from mixing these.
```python
import jax.numpy as jnp
## Honest benchmark pattern
def step(x):
return x + 1
Use `assets/benchmark_template.py` or `scripts/jax_benchmark_harness.py`.
def make_inputs():
return (jnp.arange(1024, dtype=jnp.float32),), {}
```
Checklist:
- warm up first
- block before stopping the timer
- report first-call and steady-state separately
- say whether data transfer is inside or outside the timer
- state backend and key shapes/dtypes
Use `--file PATH --function step --factory make_inputs --jit --repeat 20`.
The factory must return fresh equivalent buffers for every invocation, including
warm-up and first call. This makes explicit `--donate-argnums 0` possible without
reusing invalidated arrays. Static arguments must remain stable; setup itself
must not dominate memory or change the workload. The helper does not detect every
aliasing mistake in user factories.
## Compile-time problems
Inputs are synchronised outside timing; results are synchronised inside. Any
failure propagates rather than returning a plausible timing. Report median,
spread and raw samples, not only the fastest run. First-call time mixes multiple
costs and can hit caches. Use lower/compile probes or separate controlled processes
when isolated compilation/cold-cache measurements matter. A pre-jitted callable
can consume donated inputs even without the harness's `--jit` option.
Symptoms:
- first call is extremely slow
- every call looks like a first call
- `make_jaxpr` is huge
- CPU-side time dominates
A training loop is a different workload: carry the returned state into the next
iteration, use fresh batches/keys, and separate warm-up from steady-state throughput.
Do not keep resetting it and label the independent-input result training throughput.
For eager/JIT comparison, use separate fresh-input runs and compare correctness
and workload equivalence; donation invalidates shared comparison inputs too.
Check:
- changing shape or dtype
- changing sharding
- static arguments changing every call
- creating new lambdas/partials/jitted functions inside loops
- long Python loops inside `jit`
- giant captured constants / closures
## Diagnose before rewriting
Tools:
- `scripts/jax_compile_probe.py`
- `scripts/jax_recompile_explorer.py`
Repeated compilations: inspect changing shape/dtype/sharding/static values,
new closures/jitted functions, large captured constants and Python-loop unrolling.
Use the compile probe/recompile explorer as diagnostic aids and confirm with
compilation logs/traces; a signature heuristic is not an authoritative compile count.
Typical fixes:
- stabilise shapes
- hoist `jit`
- use `scan` or `fori_loop`
- make static args explicit and small
- avoid rebuilding objects each call
Slow execution: inspect host conversions, tiny kernel dispatches, callbacks,
input starvation, resharding, and redundant computation. Consider larger compiled
regions, batching and structured loops only against an actual profile.
## Steady-state execution problems
Memory pressure: measure live buffers and peaks, examine retained outputs,
replication, intermediates and autodiff residuals. Donation and checkpointing
have semantics and compute/memory trade-offs; neither is an automatic speed-up.
Use `jax.checkpoint` before experimental alternatives unless evidence favours them.
Symptoms:
- first call is fine, repeated calls are still slow
- GPU/TPU utilisation is poor
- code is fast in theory but not in practice
Synchronise trace windows, capture realistic iterations, and use the target
accelerator's profiler. CPU smoke tests cannot establish GPU/TPU throughput or
multi-host correctness. A persistent cache helps repeated compilation but must
be trusted and its warm/cold status included in measurements.
Check:
- host round-trips
- tiny compiled kernels separated by Python
- poor batching
- accidental replication or resharding
- heavy callbacks or printing
- slow data pipeline starving the device
Typical fixes:
- larger compiled regions
- `vmap` / `scan`
- keep arrays on device
- explicit sharding
- reduce logging / callbacks in the hot path
## Memory problems
Symptoms:
- OOM
- high peak memory
- code only works with tiny batch sizes
Check:
- large intermediates being materialised
- duplication across branches or batches
- unnecessary outputs retained
- no donation where it would help
- sharding causing replication
- activations that could be recomputed
Typical fixes:
- buffer donation
- rematerialisation (`jax.checkpoint`)
- better sharding
- smaller live ranges
- structured loops instead of unrolled Python
Do not suggest donation or remat automatically; tie them to evidence.
## Donation
Donation is useful when:
- an input buffer is dead after the call
- the output can reuse its storage
- memory pressure is real
Donation is not a magic speed-up knob. Use it primarily for memory and only after correctness and API semantics are clear.
## Persistent compilation cache
Consider it when:
- the same program is compiled repeatedly across runs
- compile time is a real user pain point
- the environment is stable enough for cache reuse to matter
This is especially relevant for development loops and repeated workloads, not as a first response to every slowdown.
## Profiling strategy
Use a profiler before deep optimisation when:
- the user wants serious speed work
- compile time is not obviously the whole story
- memory or communication may dominate
Start with:
- trace collection (`assets/profile_template.py`)
- device memory profiling for OOM or leaks
- lowering inspection if compile structure seems wrong
## Performance review questions
Ask these in order:
1. Is the code timing dispatch or actual execution?
2. Is the slow path compile, execute, transfer, or communication?
3. Are shapes/dtypes/shardings stable?
4. Is there a Python loop or callback in the hot path?
5. Is the data already on device?
6. Is sharding aligned with the algorithm?
7. Is the memory footprint forcing a bad design?
## “Fast JAX code” defaults
These defaults are often right:
- compile coarse-grained steps, not every small helper
- batch independent work with `vmap`
- express long loops with `scan`
- keep hot arrays on device
- measure with blocking
- reduce shape churn
- prefer global-view sharding before manual per-device code
- use profiler traces rather than intuition for serious tuning
## What to report back to the user
When performance is discussed, try to report:
- backend and device count
- input shapes and dtypes
- first-call time
- repeated-call summary
- what was inside the timer
- the most likely remaining bottleneck
That level of honesty is more useful than a vague “should be faster”.
See [source baseline](SOURCES.md) for current APIs and official references.
+46 -43
View File
@@ -1,50 +1,53 @@
# Sources and maintenance notes
# Source baseline and maintenance
This skill was rebuilt from two inputs:
Reviewed 2026-09-13. The public release baseline is **JAX 0.11.1**, released
2026-08-17, whose PyPI metadata requires Python **>=3.12**. There is no blanket
upper bound in that metadata; this does not guarantee wheels/backend support for
every future interpreter. Check jaxlib, accelerator plugin, driver, and platform
requirements separately. Python 3.13 free-threaded support was dropped in 0.11.0;
do not conflate it with ordinary CPython 3.13.
1. the provided agent-skill authoring guides
2. current JAX documentation plus the provided `jax-main.zip` source snapshot
Traceable upstream source: tag `jax-v0.11.1`, commit
`2d66622450e2c8633cda2307688ef7aa294bd6eb` in `jax-ml/jax`. This supersedes the
unidentified `jax-main.zip` provenance claim; it does not assert every bundled
example came from that tag or that a current-version runtime test was performed.
## JAX topics explicitly refreshed for this version
## Current changes relevant to the retained references
- installation and platform guidance
- asynchronous dispatch and honest benchmarking
- typed PRNG keys and key-reuse considerations
- control-flow primitives and `scan` / `fori_loop`
- modern sharding APIs and `pmap` migration
- export / serialisation and AOT lowering
- profiling and memory-tooling guidance
- Pallas and advanced extension points
- Prefer `with jax.set_mesh(mesh):`; the old Mesh context is deprecated.
- `NamedSharding` is a placement object. Explicit sharding-in-types requires the
appropriate mesh axis types and propagation rules; merely constructing this
object is not enough to claim explicit-mode semantics.
- Ordinary pure-function design remains a useful default. Public `jax.ref` offers
explicit stateful arrays with defined effects; inspect its rules for transforms,
autodiff, and lifetimes rather than categorically banning all state.
- `jnp.empty`/`empty_like` are uninitialised in 0.11.0+, not zero constructors.
- Current export deserialisation checks its compatibility window. Keep provenance,
producer/consumer versions and actual load/execute tests; do not bypass expiry
errors to pretend an artefact is still supported. Prefer `in_shardings_jax` and
`out_shardings_jax` over the deprecated HLO sharding fields.
- Many `jax.core`/`jax.interpreters` internals were removed. Use public interfaces
and documented `jax.extend` where appropriate; match source-level debugging to
the exact installed commit. Experimental hijax, Pallas, and custom rematerialisation
are targeted tools, not obligatory rewrites for every numerical function.
- A persistent compilation cache is trusted executable infrastructure. Do not
use a cache directory writable by untrusted users or accept arbitrary exported
executables as inert data.
## Maintenance checklist
## Primary references
When updating the skill for a newer JAX release:
- [Package metadata](https://pypi.org/project/jax/)
- [Release-tag source](https://github.com/jax-ml/jax/tree/jax-v0.11.1)
- [Changelog](https://docs.jax.dev/en/latest/changelog.html)
- [Installation](https://docs.jax.dev/en/latest/installation.html)
- [Donation](https://docs.jax.dev/en/latest/buffer_donation.html)
- [Benchmarking](https://docs.jax.dev/en/latest/benchmarking.html)
- [Export](https://docs.jax.dev/en/latest/export/export.html)
- [Refs](https://docs.jax.dev/en/latest/array_refs.html)
- [Compilation cache](https://docs.jax.dev/en/latest/persistent_compilation_cache.html)
1. re-check:
- `docs/changelog.md`
- `docs/installation.md`
- `docs/random-numbers.md`
- `docs/debugging.md`
- `docs/benchmarking.md`
- `docs/sharded-computation.md`
- `docs/migrate_pmap.md`
- `docs/export/export.md`
- `docs/device_memory_profiling.md`
2. revisit:
- `jax/_src/api.py`
- `jax/_src/random.py`
- `jax/_src/debugging.py`
- `jax/_src/pjit.py`
- `jax/_src/sharding.py`
- `jax/_src/pallas/`
3. refresh the eval prompts if terminology or recommended APIs shift
## Notes for future editors
- keep `SKILL.md` focused on workflow and escalation logic
- push deep detail into the reference files
- prefer scripts that produce structured output and avoid interactive prompts
- keep claims about performance and backend behaviour tied to evidence
- treat `pmap`, export, and Pallas guidance as likely to drift over time
For maintenance, identify the latest stable release, read its versioned changes,
then inspect the exact changed method/types. Do not copy unreleased examples into
stable guidance. Test selected numerical templates, backend features, and helper
failure paths, recording the runtime actually used. Keep static scan/evaluation
fixtures separate from measured runtime evidence.
+68 -281
View File
@@ -1,300 +1,87 @@
\
#!/usr/bin/env python3
"""Benchmark a Python callable with optional JAX JIT and proper blocking."""
"""Benchmark fresh, independent JAX inputs. Imports and executes trusted project code."""
from __future__ import annotations
import argparse
import importlib
import contextlib
import importlib.util
import json
from pathlib import Path
import statistics
import sys
import time
from pathlib import Path
from typing import Any
def load_module(module_name: str | None, file_path: str | None) -> Any:
if bool(module_name) == bool(file_path):
raise ValueError("Exactly one of --module or --file is required.")
if module_name:
return importlib.import_module(module_name)
def measure(fn, make_inputs, jax, *, repeat=10, warmup=1):
"""Factory returns (positional args, keyword args), with fresh donated buffers."""
if type(repeat) is not int or repeat < 1 or type(warmup) is not int or warmup < 0:
raise ValueError('repeat must be positive; warmup must be nonnegative')
if not callable(fn) or not callable(make_inputs):
raise TypeError('Function and input factory must be callable')
path = Path(file_path or "")
if not path.exists():
raise FileNotFoundError(f"Module file not found: {path}")
def once():
args, kwargs = make_inputs()
if not isinstance(args, (tuple, list)) or not isinstance(kwargs, dict):
raise TypeError('Input factory must return (args sequence, kwargs dictionary)')
# Preparation/transfers are excluded. Blocking errors must propagate.
jax.block_until_ready((args, kwargs))
start = time.perf_counter()
result = fn(*args, **kwargs)
jax.block_until_ready(result)
return (time.perf_counter() - start) * 1000
spec = importlib.util.spec_from_file_location(path.stem, path)
if spec is None or spec.loader is None:
raise ImportError(f"Could not load module spec from: {path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
first = once()
for _ in range(warmup):
once()
samples = [once() for _ in range(repeat)]
return {'first_call_ms': first, 'times_ms': samples,
'median_ms': statistics.median(samples), 'min_ms': min(samples),
'max_ms': max(samples), 'stdev_ms': statistics.pstdev(samples),
'input_preparation_timed': False, 'output_synchronisation_timed': True}
def resolve_attr(obj: Any, dotted: str) -> Any:
current = obj
for part in dotted.split("."):
current = getattr(current, part)
return current
def load_json_arg(raw: str | None, file_path: str | None, default: Any) -> Any:
if raw is not None and file_path is not None:
raise ValueError("Choose either the inline JSON form or the file form, not both.")
if raw is not None:
return json.loads(raw)
if file_path is not None:
return json.loads(Path(file_path).read_text(encoding="utf-8"))
return default
def numeric_tree(value: Any) -> bool:
if isinstance(value, (int, float, bool)):
return True
if isinstance(value, list):
return all(numeric_tree(v) for v in value)
return False
def maybe_import_jax():
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--file', type=Path, required=True)
parser.add_argument('--function', required=True)
parser.add_argument('--factory', required=True, help='No-argument function returning fresh (args, kwargs)')
parser.add_argument('--jit', action='store_true')
parser.add_argument('--static-argnums', default='')
parser.add_argument('--donate-argnums', default='')
parser.add_argument('--repeat', type=int, default=10)
parser.add_argument('--warmup', type=int, default=1)
options = parser.parse_args()
try:
jax = importlib.import_module("jax")
jnp = importlib.import_module("jax.numpy")
return jax, jnp
except Exception:
return None, None
def tree_arrayify(value: Any, jnp_module: Any) -> Any:
if isinstance(value, dict):
return {k: tree_arrayify(v, jnp_module) for k, v in value.items()}
if isinstance(value, list):
if numeric_tree(value):
return jnp_module.array(value)
return [tree_arrayify(v, jnp_module) for v in value]
return value
def tree_device_put(value: Any, jax_module: Any) -> Any:
if isinstance(value, dict):
return {k: tree_device_put(v, jax_module) for k, v in value.items()}
if isinstance(value, list):
return [tree_device_put(v, jax_module) for v in value]
try:
return jax_module.device_put(value)
except Exception:
return value
def parse_int_tuple(raw: str | None) -> tuple[int, ...] | None:
if raw is None or raw == "":
return None
return tuple(int(part.strip()) for part in raw.split(",") if part.strip())
def block_until_ready(value: Any, jax_module: Any | None) -> Any:
if hasattr(value, "block_until_ready"):
return value.block_until_ready()
if jax_module is not None:
try:
return jax_module.block_until_ready(value)
except Exception:
pass
return value
def run_once(fn: Any, args: list[Any], kwargs: dict[str, Any], jax_module: Any | None) -> Any:
out = fn(*args, **kwargs)
block_until_ready(out, jax_module)
return out
def summary(times_ms: list[float]) -> dict[str, float]:
return {
"mean_ms": statistics.mean(times_ms),
"median_ms": statistics.median(times_ms),
"min_ms": min(times_ms),
"max_ms": max(times_ms),
"stdev_ms": statistics.pstdev(times_ms) if len(times_ms) > 1 else 0.0,
}
def benchmark(fn: Any, args: list[Any], kwargs: dict[str, Any], repeat: int, jax_module: Any | None) -> list[float]:
times_ms = []
for _ in range(repeat):
t0 = time.perf_counter()
run_once(fn, args, kwargs, jax_module)
times_ms.append((time.perf_counter() - t0) * 1e3)
return times_ms
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Benchmark a Python callable with optional JAX JIT and proper blocking.",
formatter_class=argparse.RawTextHelpFormatter,
epilog="""Exit codes:
0 success
2 operational error
Examples:
python3 scripts/jax_benchmark_harness.py --file evals/files/naive_benchmark.py --function matmul_step \\
--args-json '[[[1.0, 2.0], [3.0, 4.0]], [[1.0], [2.0]]]' --arrayify --jit --compare-eager
python3 scripts/jax_benchmark_harness.py --module mypkg.train --function step \\
--args-file args.json --kwargs-file kwargs.json --jit --repeat 20
python3 scripts/jax_benchmark_harness.py --file train.py --function step \\
--args-file args.json --jit --static-argnums 2 --donate-argnums 0,1
""",
)
source = parser.add_mutually_exclusive_group(required=True)
source.add_argument("--module", help="Import path for the module containing the callable.")
source.add_argument("--file", help="Path to a Python file containing the callable.")
parser.add_argument("--function", required=True, help="Callable name or dotted attribute path.")
parser.add_argument("--args-json", help="JSON list of positional arguments.")
parser.add_argument("--args-file", help="Path to a JSON file containing positional arguments.")
parser.add_argument("--kwargs-json", help="JSON object of keyword arguments.")
parser.add_argument("--kwargs-file", help="Path to a JSON file containing keyword arguments.")
parser.add_argument("--arrayify", action="store_true", help="Convert numeric JSON lists to `jax.numpy.array` when JAX is available.")
parser.add_argument("--device-put", action="store_true", help="Apply `jax.device_put` to arguments before timing when JAX is available.")
parser.add_argument("--jit", action="store_true", help="Wrap the callable with `jax.jit`.")
parser.add_argument("--static-argnums", help="Comma-separated positional indices to mark static when using --jit.")
parser.add_argument("--donate-argnums", help="Comma-separated positional indices to donate when using --jit.")
parser.add_argument("--compare-eager", action="store_true", help="Also benchmark the original eager callable.")
parser.add_argument("--repeat", type=int, default=10, help="Number of timed steady-state repetitions. Default: 10")
parser.add_argument("--warmup", type=int, default=1, help="Warm-up calls before timed loops. Default: 1")
parser.add_argument("--format", choices=("json", "text"), default="json", help="Output format. Default: json")
parser.add_argument("--output", help="Write the report to this file instead of stdout.")
return parser.parse_args()
def main() -> int:
args = parse_args()
try:
module = load_module(args.module, args.file)
fn = resolve_attr(module, args.function)
if not callable(fn):
raise TypeError(f"Resolved object is not callable: {args.function}")
raw_args = load_json_arg(args.args_json, args.args_file, [])
raw_kwargs = load_json_arg(args.kwargs_json, args.kwargs_file, {})
if not isinstance(raw_args, list):
raise TypeError("Positional arguments JSON must decode to a list.")
if not isinstance(raw_kwargs, dict):
raise TypeError("Keyword arguments JSON must decode to an object.")
jax, jnp = maybe_import_jax()
proc_args = list(raw_args)
proc_kwargs = dict(raw_kwargs)
if args.arrayify:
if jax is None or jnp is None:
raise RuntimeError("--arrayify requires JAX to be importable.")
proc_args = [tree_arrayify(v, jnp) for v in proc_args]
proc_kwargs = {k: tree_arrayify(v, jnp) for k, v in proc_kwargs.items()}
if args.device_put:
if jax is None:
raise RuntimeError("--device-put requires JAX to be importable.")
proc_args = [tree_device_put(v, jax) for v in proc_args]
proc_kwargs = {k: tree_device_put(v, jax) for k, v in proc_kwargs.items()}
static_argnums = parse_int_tuple(args.static_argnums)
donate_argnums = parse_int_tuple(args.donate_argnums)
report: dict[str, Any] = {
"source": args.module or args.file,
"callable": args.function,
"repeat": args.repeat,
"warmup": args.warmup,
"jax_available": jax is not None,
"jit_requested": args.jit,
"compare_eager": args.compare_eager,
"static_argnums": static_argnums,
"donate_argnums": donate_argnums,
}
if args.compare_eager or not args.jit:
for _ in range(args.warmup):
run_once(fn, proc_args, proc_kwargs, jax)
eager_times = benchmark(fn, proc_args, proc_kwargs, args.repeat, jax)
report["eager"] = {
"times_ms": eager_times,
"summary": summary(eager_times),
}
if args.jit:
if jax is None:
raise RuntimeError("--jit requires JAX to be importable.")
jit_kwargs = {}
if static_argnums is not None:
jit_kwargs["static_argnums"] = static_argnums
if donate_argnums is not None:
jit_kwargs["donate_argnums"] = donate_argnums
fn_jit = jax.jit(fn, **jit_kwargs)
t0 = time.perf_counter()
run_once(fn_jit, proc_args, proc_kwargs, jax)
first_call_ms = (time.perf_counter() - t0) * 1e3
for _ in range(max(args.warmup - 1, 0)):
run_once(fn_jit, proc_args, proc_kwargs, jax)
jit_times = benchmark(fn_jit, proc_args, proc_kwargs, args.repeat, jax)
report["jit"] = {
"first_call_ms": first_call_ms,
"times_ms": jit_times,
"summary": summary(jit_times),
}
if "eager" in report and "jit" in report:
eager_mean = report["eager"]["summary"]["mean_ms"]
jit_mean = report["jit"]["summary"]["mean_ms"]
report["speedup_vs_eager_mean"] = (eager_mean / jit_mean) if jit_mean else None
except Exception as exc:
sys.stderr.write(f"Error: {type(exc).__name__}: {exc}\n")
if not options.jit and (options.static_argnums or options.donate_argnums):
raise ValueError('Static/donation options require --jit')
if options.repeat < 1 or options.warmup < 0:
raise ValueError('Invalid repeat/warmup count')
# Keep stdout machine-readable even when the imported module prints.
with contextlib.redirect_stdout(sys.stderr):
import jax
import jaxlib
spec = importlib.util.spec_from_file_location('_jax_benchmark_target', options.file.resolve())
if spec is None or spec.loader is None:
raise ImportError('Cannot load benchmark module')
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
fn, factory = getattr(module, options.function), getattr(module, options.factory)
indices = lambda text: tuple(int(x.strip()) for x in text.split(',') if x.strip())
if options.jit:
fn = jax.jit(fn, static_argnums=indices(options.static_argnums),
donate_argnums=indices(options.donate_argnums))
report = measure(fn, factory, jax, repeat=options.repeat, warmup=options.warmup)
report.update(jax=jax.__version__, jaxlib=jaxlib.__version__,
python=sys.version.split()[0], backend=jax.default_backend(),
devices=[str(d) for d in jax.devices()], jit=options.jit,
factory=options.factory, repeat=options.repeat, warmup=options.warmup,
note='Independent-input latency, not chained training throughput or isolated compilation time')
print(json.dumps(report, indent=2))
return 0
except Exception as error:
print(json.dumps({'ok': False, 'error': f'{type(error).__name__}: {error}'}), file=sys.stderr)
return 2
if args.format == "json":
text = json.dumps(report, indent=2, sort_keys=True)
else:
lines = [
f"Callable: {report['source']}::{report['callable']}",
f"Repeat: {report['repeat']}",
f"Warmup: {report['warmup']}",
f"JAX available: {report['jax_available']}",
f"JIT requested: {report['jit_requested']}",
f"Static argnums: {report['static_argnums']}",
f"Donate argnums: {report['donate_argnums']}",
"",
]
if "eager" in report:
lines.append("Eager")
for key, value in report["eager"]["summary"].items():
lines.append(f" {key}: {value:.3f}")
lines.append("")
if "jit" in report:
lines.append("JIT")
lines.append(f" first_call_ms: {report['jit']['first_call_ms']:.3f}")
for key, value in report["jit"]["summary"].items():
lines.append(f" {key}: {value:.3f}")
lines.append("")
if "speedup_vs_eager_mean" in report:
lines.append(f"speedup_vs_eager_mean: {report['speedup_vs_eager_mean']:.3f}")
text = "\n".join(lines)
if args.output:
Path(args.output).write_text(text + ("" if text.endswith("\n") else "\n"), encoding="utf-8")
else:
sys.stdout.write(text)
if not text.endswith("\n"):
sys.stdout.write("\n")
return 0
if __name__ == "__main__":
if __name__ == '__main__':
raise SystemExit(main())
+44 -290
View File
@@ -1,309 +1,63 @@
\
#!/usr/bin/env python3
"""Emit a structured report about the local JAX environment.
Design goals:
- non-interactive
- JSON by default
- standard-library only
- useful even when JAX is not importable
"""
"""Report versions/devices without dumping environment values. Backend probing initialises JAX."""
from __future__ import annotations
import argparse
import importlib
import importlib.metadata
import json
import os
from pathlib import Path
import platform
import sys
import time
from typing import Any
PACKAGE_NAMES = (
"jax",
"jaxlib",
"numpy",
"scipy",
"flax",
"optax",
"equinox",
"orbax-checkpoint",
)
CONFIG_KEYS = (
"jax_enable_x64",
"jax_default_matmul_precision",
"jax_debug_nans",
"jax_debug_infs",
"jax_debug_key_reuse",
"jax_platform_name",
"jax_default_prng_impl",
"jax_transfer_guard",
"jax_compilation_cache_dir",
)
ENV_PREFIXES = (
"JAX_",
"XLA_",
"CUDA_",
"NVIDIA_",
"ROCM",
"HIP_",
"TPU",
"NCCL_",
)
ENV_NAMES = {
"CUDA_VISIBLE_DEVICES",
"NVIDIA_VISIBLE_DEVICES",
"XLA_FLAGS",
"PYTHONPATH",
"LD_LIBRARY_PATH",
"PATH",
}
def package_info(name: str) -> dict[str, Any]:
out: dict[str, Any] = {"installed": False}
def build_report(smoke=False):
packages, package_errors = {}, {}
for name in ('jax', 'jaxlib', 'numpy', 'scipy', 'flax', 'optax', 'equinox', 'orbax-checkpoint'):
try: packages[name] = importlib.metadata.version(name)
except importlib.metadata.PackageNotFoundError: packages[name] = None
except Exception as error:
packages[name] = None
package_errors[name] = type(error).__name__
report = {'ok': False, 'python': platform.python_version(), 'platform': platform.platform(),
'packages': packages, 'package_errors': package_errors, 'environment_names_only': sorted(
k for k in os.environ if k.startswith(('JAX_', 'XLA_', 'CUDA_', 'NVIDIA_', 'NCCL_', 'TPU', 'ROCM', 'HIP_')))}
try:
out["version"] = importlib.metadata.version(name)
out["installed"] = True
except importlib.metadata.PackageNotFoundError:
out["version"] = None
except Exception as exc: # pragma: no cover - best effort only
out["version"] = None
out["error"] = f"{type(exc).__name__}: {exc}"
return out
def selected_environment() -> dict[str, str]:
env = {}
for key, value in os.environ.items():
if key in ENV_NAMES or any(key.startswith(prefix) for prefix in ENV_PREFIXES):
env[key] = value
return dict(sorted(env.items()))
def read_config_value(jax_module: Any, key: str) -> Any:
cfg = getattr(jax_module, "config", None)
if cfg is None:
return None
# Try the most stable public-ish access patterns first.
for getter in (
lambda: getattr(cfg, "values", {}).get(key),
lambda: cfg.read(key), # type: ignore[attr-defined]
lambda: getattr(cfg, key),
):
try:
value = getter()
if value is not None:
return value
except Exception:
pass
return None
def maybe_key(jax_module: Any, seed: int):
try:
return jax_module.random.key(seed)
except Exception:
return jax_module.random.PRNGKey(seed)
def smoke_test(jax_module: Any, jnp_module: Any) -> dict[str, Any]:
report: dict[str, Any] = {"ok": False}
t0 = time.perf_counter()
key = maybe_key(jax_module, 0)
x = jax_module.random.normal(key, (256, 256), dtype=jnp_module.float32)
y = (x @ x.T).block_until_ready()
@jax_module.jit
def loss_fn(z):
return jnp_module.sum(jnp_module.tanh(z @ z.T))
loss = loss_fn(x)
loss.block_until_ready()
grad = jax_module.grad(lambda z: jnp_module.sum(jnp_module.sin(z)))(x)
jax_module.block_until_ready(grad)
report["ok"] = True
report["elapsed_ms"] = (time.perf_counter() - t0) * 1e3
report["matmul_shape"] = tuple(int(v) for v in y.shape)
report["loss_dtype"] = str(loss.dtype)
import jax
import jax.numpy as jnp
report.update(backend=jax.default_backend(), devices=[str(d) for d in jax.devices()],
process_count=jax.process_count(), process_index=jax.process_index(),
x64=bool(jax.config.jax_enable_x64))
if smoke:
x = jnp.arange(8, dtype=jnp.float32)
output = jax.jit(lambda a: a + 1)(x)
jax.block_until_ready(output)
report['smoke_test'] = {'ok': bool(jnp.all(output == x + 1)), 'scope': 'small compiled addition'}
if not report['smoke_test']['ok']: return report
report['ok'] = not package_errors
except Exception as error:
# Exception messages may contain paths, endpoint addresses, or configuration values.
report['error_type'] = type(error).__name__
report['next_step'] = 'Inspect the original import/backend error privately in the target environment'
return report
def load_jax_report(run_smoke_test: bool) -> dict[str, Any]:
report: dict[str, Any] = {"imported": False}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--smoke-test', action='store_true')
parser.add_argument('--format', choices=('json', 'text'), default='json')
parser.add_argument('--output', type=Path)
args = parser.parse_args()
report = build_report(args.smoke_test)
text = json.dumps(report, indent=2) if args.format == 'json' else '\n'.join(f'{k}: {v}' for k, v in report.items())
try:
jax = importlib.import_module("jax")
jnp = importlib.import_module("jax.numpy")
except Exception as exc:
report["import_error"] = f"{type(exc).__name__}: {exc}"
return report
report["imported"] = True
report["version"] = getattr(jax, "__version__", None)
try:
report["default_backend"] = jax.default_backend()
except Exception:
report["default_backend"] = None
try:
report["process_count"] = int(jax.process_count())
report["process_index"] = int(jax.process_index())
report["device_count"] = int(jax.device_count())
report["local_device_count"] = int(jax.local_device_count())
except Exception as exc:
report["process_error"] = f"{type(exc).__name__}: {exc}"
devices = []
try:
for dev in jax.devices():
devices.append(
{
"id": getattr(dev, "id", None),
"platform": getattr(dev, "platform", None),
"device_kind": getattr(dev, "device_kind", None),
"process_index": getattr(dev, "process_index", None),
"memory_limit": getattr(dev, "memory_limit", None),
}
)
except Exception as exc:
report["devices_error"] = f"{type(exc).__name__}: {exc}"
report["devices"] = devices
cfg = {}
for key in CONFIG_KEYS:
cfg[key] = read_config_value(jax, key)
report["config"] = cfg
if run_smoke_test:
try:
report["smoke_test"] = smoke_test(jax, jnp)
except Exception as exc:
report["smoke_test"] = {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
return report
def build_report(run_smoke_test: bool) -> dict[str, Any]:
return {
"python": {
"version": sys.version.split()[0],
"executable": sys.executable,
},
"platform": {
"system": platform.system(),
"release": platform.release(),
"machine": platform.machine(),
"platform": platform.platform(),
},
"packages": {name: package_info(name) for name in PACKAGE_NAMES},
"environment": selected_environment(),
"jax": load_jax_report(run_smoke_test),
}
def format_text(report: dict[str, Any]) -> str:
lines = []
lines.append("Python")
lines.append(f" version: {report['python']['version']}")
lines.append(f" executable: {report['python']['executable']}")
lines.append("")
lines.append("Platform")
for key, value in report["platform"].items():
lines.append(f" {key}: {value}")
lines.append("")
lines.append("Packages")
for name, info in report["packages"].items():
status = info.get("version") if info.get("installed") else "not installed"
lines.append(f" {name}: {status}")
lines.append("")
lines.append("Environment")
for key, value in report["environment"].items():
lines.append(f" {key}={value}")
lines.append("")
jax_info = report["jax"]
lines.append("JAX")
if not jax_info.get("imported"):
lines.append(f" import_failed: {jax_info.get('import_error')}")
return "\n".join(lines)
lines.append(f" version: {jax_info.get('version')}")
lines.append(f" default_backend: {jax_info.get('default_backend')}")
lines.append(f" process_count: {jax_info.get('process_count')}")
lines.append(f" process_index: {jax_info.get('process_index')}")
lines.append(f" device_count: {jax_info.get('device_count')}")
lines.append(f" local_device_count: {jax_info.get('local_device_count')}")
lines.append("")
lines.append("Devices")
for device in jax_info.get("devices", []):
lines.append(
" - id={id} platform={platform} kind={device_kind} process_index={process_index} memory_limit={memory_limit}".format(
**device
)
)
lines.append("")
lines.append("Config")
for key, value in jax_info.get("config", {}).items():
lines.append(f" {key}: {value}")
smoke = jax_info.get("smoke_test")
if smoke:
lines.append("")
lines.append("Smoke test")
for key, value in smoke.items():
lines.append(f" {key}: {value}")
return "\n".join(lines)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Emit a structured report about the local JAX environment.",
formatter_class=argparse.RawTextHelpFormatter,
epilog="""Exit codes:
0 success
2 operational error
Examples:
python3 scripts/jax_env_report.py
python3 scripts/jax_env_report.py --smoke-test --format text
python3 scripts/jax_env_report.py --output env.json
""",
)
parser.add_argument("--smoke-test", action="store_true", help="Run a small JAX smoke test if JAX is importable.")
parser.add_argument("--format", choices=("json", "text"), default="json", help="Output format. Default: json")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
return parser.parse_args()
def main() -> int:
args = parse_args()
try:
report = build_report(args.smoke_test)
text = json.dumps(report, indent=2, sort_keys=True) if args.format == "json" else format_text(report)
if args.output:
with open(args.output, "w", encoding="utf-8") as f:
f.write(text)
if not text.endswith("\n"):
f.write("\n")
else:
sys.stdout.write(text)
if not text.endswith("\n"):
sys.stdout.write("\n")
return 0
except Exception as exc:
sys.stderr.write(f"Error: {type(exc).__name__}: {exc}\n")
descriptor = os.open(args.output, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(descriptor, 'w', encoding='utf-8') as stream: stream.write(text + '\n')
else: print(text)
except OSError as error:
print(json.dumps({'ok': False, 'error_type': type(error).__name__}), file=sys.stderr)
return 2
return 0 if report['ok'] else 2
if __name__ == "__main__":
raise SystemExit(main())
if __name__ == '__main__': raise SystemExit(main())
+54
View File
@@ -0,0 +1,54 @@
import importlib.util
from pathlib import Path
import unittest
spec = importlib.util.spec_from_file_location('bench', Path(__file__).parents[1] / 'scripts/jax_benchmark_harness.py')
bench = importlib.util.module_from_spec(spec)
spec.loader.exec_module(bench)
class FakeJax:
def __init__(self): self.blocks = []
def block_until_ready(self, value): self.blocks.append(value)
class BenchmarkTests(unittest.TestCase):
def test_fresh_inputs_for_every_call(self):
created = []
def factory():
value = {'used': False}
created.append(value)
return ([value], {})
def donated(value):
self.assertFalse(value['used'])
value['used'] = True
return {'result': 1}
fake = FakeJax()
result = bench.measure(donated, factory, fake, repeat=3, warmup=2)
self.assertEqual(len(created), 6)
self.assertEqual(len(fake.blocks), 12)
self.assertEqual(len(result['times_ms']), 3)
def test_block_failure_is_not_swallowed(self):
class Broken:
def block_until_ready(self, value): raise RuntimeError('device failure')
with self.assertRaisesRegex(RuntimeError, 'device failure'):
bench.measure(lambda x: x, lambda: ([1], {}), Broken())
def test_output_failure_propagates(self):
class Broken:
def block_until_ready(self, value):
if value == 'output': raise RuntimeError('asynchronous failure')
with self.assertRaisesRegex(RuntimeError, 'asynchronous failure'):
bench.measure(lambda: 'output', lambda: ([], {}), Broken())
def test_invalid_counts_fail_before_factory(self):
for repeat, warmup in [(0, 1), (1, -1), (True, 1), (1, 1.5)]:
with self.assertRaises(ValueError):
bench.measure(None, None, FakeJax(), repeat=repeat, warmup=warmup)
def test_malformed_factory_fails(self):
with self.assertRaises(TypeError):
bench.measure(lambda: None, lambda: ('bad', {}), FakeJax())
def test_actual_cpu_donation(self):
import jax
import jax.numpy as jnp
fn = jax.jit(lambda x: x + 1, donate_argnums=(0,))
result = bench.measure(fn, lambda: ([jnp.arange(8, dtype=jnp.float32)], {}), jax, repeat=3)
self.assertEqual(len(result['times_ms']), 3)
if __name__ == '__main__': unittest.main()
+22
View File
@@ -0,0 +1,22 @@
import importlib.util
import json
from pathlib import Path
import unittest
from unittest.mock import patch
spec = importlib.util.spec_from_file_location('env_report', Path(__file__).parents[1] / 'scripts/jax_env_report.py')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
class EnvironmentTests(unittest.TestCase):
def test_secret_values_not_emitted(self):
with patch.dict('os.environ', {'JAX_TEST_TOKEN':'not-for-the-report', 'XLA_TEST_PATH':'/private/test-path'}):
report = module.build_report()
self.assertNotIn('not-for-the-report', json.dumps(report))
self.assertNotIn('/private/test-path', json.dumps(report))
self.assertIn('JAX_TEST_TOKEN', report['environment_names_only'])
def test_cpu_smoke(self):
report = module.build_report(True)
self.assertTrue(report['ok'])
self.assertTrue(report['smoke_test']['ok'])
if __name__ == '__main__': unittest.main()
@@ -0,0 +1,16 @@
import json
import unittest
from unittest.mock import patch
from test_env import module
class EnvironmentReviewTests(unittest.TestCase):
def test_nvidia_names_without_values(self):
with patch.dict('os.environ',{'NVIDIA_VISIBLE_DEVICES':'secret-device-set'}):
report=module.build_report()
self.assertIn('NVIDIA_VISIBLE_DEVICES',report['environment_names_only'])
self.assertNotIn('secret-device-set',json.dumps(report))
def test_unreadable_package_metadata_is_structured_and_not_ok(self):
with patch.object(module.importlib.metadata,'version',side_effect=PermissionError('private-message')):
report=module.build_report()
self.assertFalse(report['ok']);self.assertEqual(report['package_errors']['jax'],'PermissionError')
self.assertNotIn('private-message',json.dumps(report))