jax-keyguard
Catches PRNG key reuse in JAX code.
In JAX, a key passed to a random function gives the same numbers every time. If you forget to split it before using it again, your "random" values repeat and nothing complains. This package finds that mistake, either by reading your code or while it runs.
pip install jax-keyguard
Requires Python 3.10 or newer and JAX 0.4.18 or newer.
Why not JAX's own check?
JAX 0.4.26 added a jax_debug_key_reuse option, but it only checks keys made
with jax.random.key. Keys made with jax.random.PRNGKey, which most existing
code and tutorials use, are not checked, even in the latest JAX. Use this
package if:
- your code uses
jax.random.PRNGKeykeys, on any JAX version, - you are on a JAX version older than 0.4.26, which has no check at all, or
- you want to find reuse in CI without running the code. The static checker only reads your source, so it works with any key type and any JAX version.
If your code only uses jax.random.key keys on a recent JAX, turn on
jax_debug_key_reuse for runtime checking. It also works inside jit, which
the runtime check here does not. The static checker is still useful in CI.
Checking your code
Say src/model.py contains this:
import jax
def init(key):
w = jax.random.normal(key, (3, 3))
b = jax.random.normal(key, (3,))
return w, b
@jax.jit
def dropout(key, x):
keys = jax.random.split(key)
mask = jax.random.bernoulli(key, 0.9, x.shape)
return x * mask
Run the checker on a file or a directory:
$ jax-keyguard src/
src/model.py:6:27: KG001 PRNG key 'key' reused: already consumed by jax.random.normal on line 5; split it first
src/model.py:13:33: KG001 PRNG key 'key' reused: already consumed by jax.random.split on line 12; split it first
The exit status is 1 when something is found and 2 on an error, such as a file that can't be read or parsed (the other files are still checked), so it can run in CI as is. The code is never executed, which means jitted functions are checked too.
The fix is to split first and use each subkey once:
def init(key):
w_key, b_key = jax.random.split(key)
w = jax.random.normal(w_key, (3, 3))
b = jax.random.normal(b_key, (3,))
return w, b
With flake8
Installing the package registers it with flake8 under the code KG001:
$ flake8 --select KG src/
Ignoring a line
Add # noqa to the line (or # noqa: KG001 when using flake8).
From Python
from jax_keyguard import check_source
for line, col, message in check_source(open("src/model.py").read()):
print(line, message)
What it catches
Inside each function it keeps track of variables holding keys: anything
returned by jax.random.key, PRNGKey, split, fold_in or clone, loop
variables over those, plain copies like k2 = key, and parameters the function
passes to jax.random. A key counts as used when it goes into a jax.random
sampler or split. These are reported:
x = jax.random.normal(key)
y = jax.random.uniform(key) # same key twice
k1, k2 = jax.random.split(key)
z = jax.random.normal(key) # key used after splitting it
for _ in range(steps):
noise = jax.random.normal(key) # same key on every iteration
samples = [jax.random.normal(key) for _ in range(3)]
keys = jax.random.split(key, 3)
for k in keys:
a = jax.random.normal(k)
for k in keys:
b = jax.random.uniform(k) # the same three keys again
These are fine:
for step in range(steps):
step_key = jax.random.fold_in(key, step) # fold_in doesn't use up the key
if training:
x = jax.random.normal(key)
else:
x = jax.random.uniform(key) # one use per branch
samples = [jax.random.normal(k) for k in jax.random.split(key, 3)]
What it misses
- Keys kept in attributes or containers, like
self.keyorkeys[0]. - Keys returned by your own functions, like
key = make_key(seed). - Reuse through your own functions. Calling
helper(key)twice is not reported, because helpers often callfold_inon the key, which is fine. - Keys a nested function or lambda uses from the enclosing one.
- Keys used indirectly:
functools.partial(jax.random.normal, key),jax.vmap(jax.random.normal)(keys), or passed along as*args. - Different keys that happen to have the same value, like
jax.random.fold_in(key, 0)inside a loop.
The runtime check below covers most of these for code that actually runs.
It also doesn't know which way an if goes or how many times a loop runs, so
it reports a reuse that could happen on any path. A key used in a loop that
only ever runs once, or in an except block that can't be reached, still
counts. Add # noqa in those cases.
Checking at runtime
This raises KeyReuseError as soon as a key is used a second time. It works
with both kinds of keys, jax.random.key and the older jax.random.PRNGKey,
on any supported JAX version.
In tests
Each test starts with a clean slate, so tests that all begin with
PRNGKey(0) don't interfere with each other.
$ pytest --keyguard
...
E jax_keyguard.runtime.KeyReuseError: jax.random.uniform received a PRNG key that was already consumed by jax.random.normal at /path/to/tests/test_model.py:6. Split the key and use each subkey only once.
In a script
import jax_keyguard
jax_keyguard.enable()
Turn it off again with jax_keyguard.disable(), or call
jax_keyguard.reset() to forget the keys used so far. To check only part of
the code, use a with block:
with jax_keyguard.key_reuse_guard():
run_experiment()
Things to know
- Keys are compared by value, so two keys made from the same seed count as
one key. That includes
key(0)andPRNGKey(0), a key and itskey_data, and a key and itsjax.random.clone. The static checker treats a clone as a new key. - Nothing is checked while JAX traces a function for
jit,vmap,grad,scanand so on, because that code runs once per trace rather than once per call. Use JAX'sjax_debug_key_reusewithjax.random.keykeys there, or rely on the static checker. - It slows down random calls. Each checked call copies the key back from the
device, which also stops JAX from dispatching work ahead asynchronously. A
loop of
splitandnormalon CPU ran about twice as slowly with checking on. It is meant for tests and debugging, not for full training runs. - It remembers every key used since checking was turned on (or since the last
reset()), so memory grows by a little with each random call. That is fine in tests, where--keyguardresets before each test. In a long script, calljax_keyguard.reset()now and then, for example once per epoch, keeping in mind that reuse across a reset is not caught. - It works by replacing the functions on the
jax.randommodule. A name imported earlier withfrom jax.random import normalis not checked. - Reuse inside a
jax.pure_callbackshows up as an XLA error that wraps theKeyReuseError. - It is not thread-safe.
License
Apache-2.0
Metadata
Release files for jax-keyguard 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| jax_keyguard-0.2.1.tar.gz | 21.4 kB | Without provenance | Details for jax_keyguard-0.2.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| jax_keyguard-0.2.1-py3-none-any.whl | Python 3 | none | any | Details for jax_keyguard-0.2.1-py3-none-any.whl |
Total release size: 40.6 kB
Release files / jax_keyguard-0.2.1.tar.gz
| Download URL | jax_keyguard-0.2.1.tar.gz |
|---|---|
| Size | 21.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7683b690131cb18dd6db41f3c0367d3f214ccf97f99a6f861df924649681dba8
|
|
BLAKE2b-256 checksum How to use checksums |
778ba7ca7222be38a794c0e38dba684e360eed33650bd44503ee3beb49f1eeb1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.3
|
Release files / jax_keyguard-0.2.1-py3-none-any.whl
| Download URL | jax_keyguard-0.2.1-py3-none-any.whl |
|---|---|
| Size | 19.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3fa8cadf2ba0e2011bce556d1f80abe3adcc5a34eb139310a3b613ee569b6c08
|
|
BLAKE2b-256 checksum How to use checksums |
a32264508804f54c45db7b34f8550f0d4053424dcc1fae03e3d7e133b77d66d8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.3
|
Security for jax-keyguard 0.2.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.