Runtime¤
substrax.runtime declares the settings a JAX process starts with and applies them at the
point where they still take effect.
import os
import subprocess
import sys
from substrax.runtime import JaxRuntime, apply_runtime, runtime_environment
runtime = JaxRuntime(platforms=("cpu",), cpu_devices=8, enable_x64=True)
# A child process on eight emulated CPU devices.
env = {**os.environ, **runtime_environment(runtime, os.environ)}
subprocess.run([sys.executable, "train.py"], env=env, check=True)
# This process, before it imports jax.
apply_runtime(runtime)
import jax
assert jax.device_count() == 8
runtime_environment renders each field as the variable jax or XLA reads when it starts:
| Field | Variable |
|---|---|
platforms |
JAX_PLATFORMS |
cpu_devices |
JAX_NUM_CPU_DEVICES, which jax prefers over --xla_force_host_platform_device_count |
enable_x64 |
JAX_ENABLE_X64 |
matmul_precision |
JAX_DEFAULT_MATMUL_PRECISION |
compilation_cache_dir |
JAX_COMPILATION_CACHE_DIR |
xla_flags |
XLA_FLAGS, merged by flag name |
preallocate |
XLA_PYTHON_CLIENT_PREALLOCATE |
memory_fraction |
XLA_CLIENT_MEM_FRACTION |
A requested XLA flag that is already set to a different value raises XlaFlagConflictError,
so a device count or GPU flag exported by the caller is never silently replaced. A memory
fraction raises while the deprecated XLA_PYTHON_CLIENT_MEM_FRACTION is still set, because
jax refuses both at once. JaxRuntime applies no per-backend flag presets: xla_flags is empty
unless a run names the flags it needs.
Once jax is imported, apply_runtime applies the CPU device count, 64-bit types, matmul
precision and compilation cache directory through jax.config; jax refuses a new device count
after its first operation, and that refusal is raised as RuntimeConfigurationError.
Platforms, XLA flags and accelerator memory settings are read only when jax starts its
backends, and jax gives no public signal of whether that has happened, so after the import
they raise instead of being written where they may change nothing.
A test suite chooses its backend and emulated CPU devices before anything imports jax:
# conftest.py
import importlib.util
import os
from substrax.runtime import resolve_test_runtime, runtime_environment
runtime = resolve_test_runtime(
os.environ,
prefix="DATARAX_TEST_",
cuda_plugin_available=importlib.util.find_spec("jax_cuda12_plugin") is not None,
)
os.environ.update(runtime_environment(runtime, os.environ))
Tests run on eight emulated CPU devices unless DATARAX_TEST_JAX_PLATFORMS asks for an
accelerator or DATARAX_TEST_DEVICE_COUNT names another count; an inherited
JAX_PLATFORMS does not move them onto a GPU.
Entry points configure logging inside main(), never at import:
from substrax.runtime import configure_entry_point_logging
def main() -> None:
configure_entry_point_logging()
A second call replaces only the handler the first one installed, so it needs no force=True
and leaves pytest's log capture in place. Importing substrax.runtime imports no jax and
changes no process state.
The managed environment file¤
A repository's setup.sh writes the file its activate.sh sources once the project environment
is synced:
uv run --no-sync python -m substrax.runtime.managed_env write --prefix DATARAX \
--backend auto --output .datarax.env --set TF_CPP_MIN_LOG_LEVEL=1
python -m substrax.runtime.managed_env show --prefix DATARAX --env-file .datarax.env \
--user-env .env --user-env .env.local
auto resolves to cuda12 on Linux when nvidia-smi lists a GPU, metal on Apple Silicon,
and cpu otherwise. The file names every variable it manages in <PREFIX>_MANAGED_ENV_VARS,
so activate.sh unsets them before sourcing a newer file, then exports or unsets each:
| Variable | cpu |
cuda12, metal |
|---|---|---|
<PREFIX>_BACKEND, <PREFIX>_ENV_ROOT |
the resolved backend and the project root | the same |
JAX_PLATFORMS |
cpu |
unset, so jax picks the accelerator |
JAX_ENABLE_X64 |
false |
false |
XLA_PYTHON_CLIENT_PREALLOCATE |
unset | false |
XLA_CLIENT_MEM_FRACTION |
unset | 0.75 |
XLA_PYTHON_CLIENT_MEM_FRACTION |
unset | unset, because jax refuses it beside the current name |
jaxlib reads preallocation and the memory fraction only when it builds the GPU plugin's options,
so they are unset on the CPU. Each --set NAME=VALUE adds a repository variable to the file; a
--set naming a variable the file manages itself raises.
substrax.runtime
¤
JAX process configuration, declared once and applied before or after jax is imported.
JaxRuntime describes the settings. runtime_environment renders them as the environment of a
process that has not imported jax yet (a child process, or a conftest before its first
import jax), child_environment builds a child's whole environment from them, apply_runtime applies them to the current process, and
resolve_test_runtime chooses the backend and emulated CPU devices of a test run.
configure_entry_point_logging sets up the root logger from an entry point. Importing this
package imports no jax and changes no process state.
JaxRuntime(*, platforms=None, cpu_devices=None, enable_x64=None, matmul_precision=None, compilation_cache_dir=None, xla_flags=(), preallocate=None, memory_fraction=None)
dataclass
¤
Declarative JAX process settings; None leaves the process's current value alone.
:func:~substrax.runtime.runtime_environment renders the settings as the environment
variables jax and XLA read when a process starts, and
:func:~substrax.runtime.apply_runtime applies them to the current process.
Attributes:
| Name | Type | Description |
|---|---|---|
platforms |
tuple[str, ...] | None
|
The backends jax initialises, the first being the default ( |
cpu_devices |
int | None
|
The number of CPU devices ( |
enable_x64 |
bool | None
|
Whether 64-bit types are enabled ( |
matmul_precision |
MatmulPrecision | None
|
The default matrix-multiplication precision
( |
compilation_cache_dir |
Path | None
|
The absolute directory of the persistent compilation cache
( |
xla_flags |
tuple[str, ...]
|
Flags merged into |
preallocate |
bool | None
|
Whether the accelerator client preallocates its memory
( |
memory_fraction |
float | None
|
The fraction of accelerator memory the client may use, in |
__post_init__()
¤
Validate every field.
Raises:
| Type | Description |
|---|---|
TypeError
|
If a field holds a value of the wrong type. |
ValueError
|
If a field holds a value outside its domain. |
XlaFlagConflictError
|
If one requested flag is given two different values. |
RuntimeConfigurationError
¤
Bases: RuntimeError
A JAX process setting cannot be applied, or conflicts with one already in place.
XlaFlagConflictError(flag, existing, requested)
¤
Bases: RuntimeConfigurationError
One XLA flag was given two different values.
Attributes:
| Name | Type | Description |
|---|---|---|
flag |
str
|
The flag name, with its leading dashes. |
existing |
str | None
|
The value already in place; |
requested |
str | None
|
The conflicting value; |
Record the flag and both of its values.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
flag
|
str
|
The flag name, with its leading dashes. |
required |
existing
|
str | None
|
The value already in place. |
required |
requested
|
str | None
|
The conflicting value. |
required |
apply_runtime(runtime, *, environ=None)
¤
Apply runtime to the current process.
Before jax is imported, every field is written to environ as the variables jax and XLA
read at start-up (see :func:~substrax.runtime.runtime_environment). Once jax is imported,
the CPU device count, 64-bit types, matmul precision and compilation cache directory go
through jax.config.update; jax refuses a new device count once its backends have started.
Platforms, XLA flags and client memory settings are read only when the backends start, and jax
offers no public signal of whether that has happened, so once jax is imported they raise
instead of being written where they might change nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
runtime
|
JaxRuntime
|
The settings to apply. |
required |
environ
|
MutableMapping[str, str] | None
|
The environment written before jax is imported; |
None
|
Raises:
| Type | Description |
|---|---|
RuntimeConfigurationError
|
If jax is imported and |
XlaFlagConflictError
|
If a requested flag has a different value in the current
|
configure_entry_point_logging(level=logging.INFO, *, stream=None, fmt='%(message)s')
¤
Send log records to stream without replacing any other handler.
Call it inside main(), never at import. A second call replaces only the handler the first
one installed and leaves every other handler, pytest's log capture included, in place, so it
needs no force=True.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
level
|
int
|
The level set on the root logger. |
INFO
|
stream
|
TextIO | None
|
Where records are written; standard error when |
None
|
fmt
|
str
|
The |
'%(message)s'
|
child_environment(runtime, env=None, *, parent=None)
¤
Return the complete environment of a child process whose JAX settings the caller chooses.
The child gets parent without any JAX_* or XLA_* variable, so settings exported in
the shell that started this process cannot change what the child runs, then env, then
runtime as :func:runtime_environment renders it on top of the two.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
runtime
|
JaxRuntime
|
The child's JAX settings. |
required |
env
|
Mapping[str, str] | None
|
Variables set in the child before |
None
|
parent
|
Mapping[str, str] | None
|
The environment the child inherits; this process's when |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, str]
|
Variable names mapped to their values. |
Raises:
| Type | Description |
|---|---|
XlaFlagConflictError
|
If a flag in |
RuntimeConfigurationError
|
If |
resolve_test_runtime(env, *, prefix, cuda_plugin_available, default_cpu_devices=8)
¤
Choose the backend and the emulated CPU devices of a test run.
Tests run on the CPU unless {prefix}JAX_PLATFORMS names an accelerator, which is then used
without CPU emulation. An inherited JAX_PLATFORMS, such as the cuda,cpu a developer
shell exports, does not move tests onto an accelerator, so a local run matches CI. The CPU
device count is {prefix}DEVICE_COUNT when set (0 for no emulation), otherwise the
count JAX_NUM_CPU_DEVICES or the XLA device-count flag already names, otherwise
default_cpu_devices.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
env
|
Mapping[str, str]
|
The process environment. |
required |
prefix
|
str
|
The prefix of the repository's test variables, such as |
required |
cuda_plugin_available
|
bool
|
Whether a JAX CUDA plugin is installed. |
required |
default_cpu_devices
|
int
|
The emulated device count when nothing else names one. |
8
|
Returns:
| Type | Description |
|---|---|
JaxRuntime
|
The runtime of the test process. |
Raises:
| Type | Description |
|---|---|
RuntimeConfigurationError
|
If CUDA is requested and no JAX CUDA plugin is installed. |
ValueError
|
If |
runtime_environment(runtime, base)
¤
Return the environment variables that apply runtime to a process not yet running jax.
Only the variables runtime sets are returned, and base is read, never changed. A
field replaces the variable base holds, except xla_flags, which merges into base's
XLA_FLAGS by flag name. Pass the result to a child process on top of its environment, or
write it to os.environ before importing jax.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
runtime
|
JaxRuntime
|
The settings to render. |
required |
base
|
Mapping[str, str]
|
The environment the variables will be added to. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, str]
|
Variable names mapped to their values. |
Raises:
| Type | Description |
|---|---|
RuntimeConfigurationError
|
If |
XlaFlagConflictError
|
If a requested flag has a different value in |
merge_xla_flags(existing, requested)
¤
Merge requested flags into an XLA_FLAGS value by flag name.
Flags already present keep their position, and new flags follow in the order requested. A requested flag already set to the same value changes nothing, and one set to a different value raises, so a value already in the environment is never silently replaced.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
existing
|
str
|
The current |
required |
requested
|
Iterable[str]
|
Flags to add, each |
required |
Returns:
| Type | Description |
|---|---|
str
|
The merged |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a token is not a flag. |
XlaFlagConflictError
|
If a requested flag has a different value from an existing one. |
parse_xla_flags(value)
¤
Parse an XLA_FLAGS value into flag names mapped to their values, in order.
Flags are separated by whitespace. --name=value maps --name to value, and a bare
--name maps it to None. A flag given twice with the same value counts once.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
str
|
The |
required |
Returns:
| Type | Description |
|---|---|
dict[str, str | None]
|
Each flag name, with its leading dashes, mapped to its value. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a token is not a flag. |
XlaFlagConflictError
|
If one flag is given two different values. |
substrax.runtime.managed_env
¤
The managed environment file a repository's setup.sh writes and its activate.sh sources.
Run as python -m substrax.runtime.managed_env write --prefix DATARAX --backend auto --output
.datarax.env after the project environment is synced. The file declares every variable it
manages in <PREFIX>_MANAGED_ENV_VARS, so activate.sh can unset them before sourcing a new
one, then exports or unsets each. The JAX settings come from :class:JaxRuntime through
:func:runtime_environment, so the file uses the names jax reads today: the memory fraction is
XLA_CLIENT_MEM_FRACTION, and the deprecated XLA_PYTHON_CLIENT_MEM_FRACTION, which jaxlib
refuses beside it, is always unset. Preallocation and the memory fraction apply only to an
accelerator backend: jaxlib reads them when it builds the GPU plugin's options, so on the CPU they
are unset. A repository adds its own variables (for example TensorFlow's log level) with --set.
Importing this module imports no jax.
nvidia_gpu_visible()
¤
Whether nvidia-smi lists at least one GPU.
resolve_backend(requested)
¤
The concrete backend for requested, resolving auto from the host.
managed_environment(*, prefix, backend, project_root, extra)
¤
Every managed variable mapped to its value, or None for a variable the file unsets.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prefix
|
str
|
The repository's variable prefix, such as |
required |
backend
|
str
|
One of :data: |
required |
project_root
|
Path
|
The repository root, written as |
required |
extra
|
Mapping[str, str]
|
The repository's own variables, written as given. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, str | None]
|
The managed variables in the order they are written. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the prefix is not an upper-case shell name, the backend is unknown, or
|
render(*, prefix, values)
¤
The shell text of a managed environment file: the manifest, then one line per variable.
main(argv=None)
¤
Run the write or show command.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
argv
|
Sequence[str] | None
|
The arguments after the module name; the process arguments when |
None
|
Returns:
| Type | Description |
|---|---|
int
|
The exit status. |