From e3b88bb12a4f696dbbeade16db16042af7327d9b Mon Sep 17 00:00:00 2001 From: Junda Su <90978028+Davids048@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:50:45 -0700 Subject: [PATCH] [feat] Add a typed environment-variable registry, policy doc, and contract test (#1896) --- .agents/skills/env-var-conventions/SKILL.md | 71 ++ docs/contributing/env_vars.md | 180 ++++ .../fastvideo_kernel/block_sparse_attn.py | 18 +- .../backends/video_sparse_attn_h3.py | 8 +- fastvideo/attention/selector.py | 6 +- .../attention/utils/flash_attn_default.py | 2 +- .../attention/utils/flash_attn_no_pad.py | 2 +- .../device_communicators/ulysses_a2a.py | 2 +- fastvideo/distributed/parallel_state.py | 6 +- fastvideo/envs.py | 732 ++++++++-------- fastvideo/eval/models.py | 2 +- fastvideo/fastvideo_args.py | 10 +- fastvideo/hooks/activation_trace.py | 10 +- fastvideo/logger.py | 8 +- fastvideo/models/dits/minimax_h3.py | 4 +- fastvideo/models/loader/fsdp_load.py | 3 +- fastvideo/pipelines/composed_pipeline_base.py | 4 +- fastvideo/pipelines/stages/base.py | 2 +- fastvideo/pipelines/stages/denoising.py | 2 +- fastvideo/platforms/cuda.py | 2 +- fastvideo/platforms/npu.py | 4 +- fastvideo/platforms/rocm.py | 2 +- fastvideo/profiler.py | 22 +- fastvideo/tests/contract/test_env_policy.py | 827 ++++++++++++++++++ fastvideo/tests/stages/_denoising_fixtures.py | 6 +- fastvideo/train/methods/base.py | 2 +- fastvideo/train/models/ltx2/ltx2.py | 2 +- fastvideo/train/models/wan/wan.py | 2 +- fastvideo/training/distillation_pipeline.py | 4 +- .../self_forcing_distillation_pipeline.py | 4 +- fastvideo/training/training_pipeline.py | 9 +- fastvideo/utils.py | 10 +- fastvideo/worker/gpu_worker.py | 2 +- fastvideo/worker/multiproc_executor.py | 4 +- fastvideo/worker/ray_distributed_executor.py | 4 +- fastvideo/worker/ray_env.py | 2 +- mkdocs.yml | 1 + 37 files changed, 1546 insertions(+), 435 deletions(-) create mode 100644 .agents/skills/env-var-conventions/SKILL.md create mode 100644 docs/contributing/env_vars.md create mode 100644 fastvideo/tests/contract/test_env_policy.py diff --git a/.agents/skills/env-var-conventions/SKILL.md b/.agents/skills/env-var-conventions/SKILL.md new file mode 100644 index 000000000..d793281a5 --- /dev/null +++ b/.agents/skills/env-var-conventions/SKILL.md @@ -0,0 +1,71 @@ +--- +name: env-var-conventions +description: Add, read, rename, or remove an environment variable in FastVideo, or change the environment-variable policy. Use before touching fastvideo/envs.py, os.environ, os.getenv, or monkeypatch.setenv in fastvideo/, and when fastvideo/tests/contract/test_env_policy.py fails. +--- + +# Environment Variable Conventions + +## Purpose + +FastVideo registers its environment variables as typed fields in +`fastvideo/envs.py`. The policy that governs them is +`docs/contributing/env_vars.md`, and the contract test +`fastvideo/tests/contract/test_env_policy.py` enforces the policy in the unit +CI lane. This skill routes an environment-variable change through that policy. +The policy doc is the single source of the rules; read it instead of relying +on a summary here. + +## Prerequisites + +- Read `docs/contributing/env_vars.md` in full. +- Decide whether the setting belongs in an environment variable or an argument + (rule 5 in the policy doc). Settings that users change per deployment are + arguments; add them through `fastvideo/fastvideo_args.py` instead. + +## Inputs + +| Parameter | Required | Description | +| ---------- | -------- | -------------------------------------------------------------- | +| `change` | Yes | Add, read, rename, or remove a variable, or change the policy. | +| `variable` | Yes | The variable name, with the `FASTVIDEO_` prefix. | + +## Steps + +1. **Declare or edit the variable in `fastvideo/envs.py`.** + - Pick the field type and category that the policy doc lists. + - Write a description that states what the variable does and its units. +2. **Read the variable with `envs.NAME.get()` inside a function.** + - In tests, change the value with `envs.NAME.override(value)`. + - Do not call `os.environ`, `os.getenv`, or `monkeypatch.setenv` for a + FastVideo variable. +3. **Regenerate the table in the policy doc.** + - Run `python fastvideo/tests/contract/test_env_policy.py`. +4. **Run the contract test.** + - Run `pytest fastvideo/tests/contract/test_env_policy.py`. + - When the test reports a fixed known violation, delete or lower its entry + in `KNOWN_VIOLATIONS`. Never add an entry to `KNOWN_VIOLATIONS`. +5. **When the policy itself changes, update the policy doc and the contract + test in the same pull request.** + - The rules in `docs/contributing/env_vars.md`, the checks and allowlist in + `fastvideo/tests/contract/test_env_policy.py`, and this skill must agree. + +## Outputs + +- A registry entry in `fastvideo/envs.py` and call sites that use + `envs.NAME.get()`. +- A regenerated table in `docs/contributing/env_vars.md`. +- A passing `fastvideo/tests/contract/test_env_policy.py`. + +## Example Usage + +``` +Add a FASTVIDEO_DEBUG_MY_STAGE switch that logs MyStage inputs. +``` + +## References + +- `docs/contributing/env_vars.md`: the policy, the field types, and the + violation kinds that the contract test reports. +- `fastvideo/envs.py`: the registry. +- `fastvideo/tests/contract/test_env_policy.py`: the contract test, + `EXTERNAL_ALLOWLIST`, and `KNOWN_VIOLATIONS`. diff --git a/docs/contributing/env_vars.md b/docs/contributing/env_vars.md new file mode 100644 index 000000000..a4dac3d29 --- /dev/null +++ b/docs/contributing/env_vars.md @@ -0,0 +1,180 @@ +# Environment Variables + +FastVideo reads environment variables for expert switches, debugging, profiling, and the settings that launchers such +as `torchrun` provide. This page is the policy for those variables. The contract test +`fastvideo/tests/contract/test_env_policy.py` enforces the policy in the unit CI lane, and the coding-agent skill +`.agents/skills/env-var-conventions/SKILL.md` points here. When the policy changes, update this page and the contract +test in the same pull request. + +## Rules + +1. **Register every FastVideo variable in `fastvideo/envs.py`.** Each entry declares a type, a default, a category, + and a description. Variables that other tools own (CUDA, NCCL, PyTorch, launchers) are not registered; code reads + them directly with `os.environ.get("NAME")`, and the name must be in the external-variable allowlist + (`EXTERNAL_ALLOWLIST` in the contract test). +2. **Read with `envs.NAME.get()`, write with `envs.NAME.set()`, and change a value in tests with + `envs.NAME.override()`.** Each type has one parsing rule. A value that the rule rejects raises + `fastvideo.envs.EnvVarError` instead of falling back to the default. +3. **Name FastVideo variables with the `FASTVIDEO_` prefix.** The second word states the purpose where one applies: + `ENABLE_`, `DISABLE_`, `USE_`, `FORCE_`, `DEBUG_`, `TEST_`. +4. **Keep a renamed variable as a deprecated alias until the next minor release.** Setting the old name logs a + warning. Delete a variable that no code reads, and list it in a deprecation table so that setting it logs a + warning. +5. **Give each setting one source: an argument or an environment variable.** Settings that users change per + deployment are arguments (CLI or YAML). Expert switches, emergency off switches, and debugging and test switches + are environment variables. +6. **Read variables inside functions.** `envs.NAME.get()` runs when the function runs, so a changed value takes + effect without re-importing a module. Module level, class bodies, decorators, and default argument values run at + import time. +7. **Do not write the environment to pass values between parts of FastVideo.** Pass an argument instead. Tests use + `envs.NAME.override()`. + +## Field types + +| Class | Value type | Parsing rule | +| ------------ | ---------------- | ----------------------------------------------------------------------------------- | +| `EnvBool` | `bool` | `1`, `true`, `yes`, `on` are true; `0`, `false`, `no`, `off`, and `""` are false. | +| | | Case-insensitive; surrounding whitespace is ignored. | +| `EnvInt` | `int` | `int(value)` | +| `EnvFloat` | `float` | `float(value)` | +| `EnvStr` | `str` or `None` | The raw string. A `None` default means that the variable has no default. | +| `EnvPath` | `str` or `None` | The raw string with a leading `~` expanded. | +| `EnvChoice` | `str` | Stripped and lower-cased, then checked against the declared `choices`. | + +A default can be a zero-argument function; `get()` calls it on each read while the variable is unset. The path roots +use this to follow `XDG_CONFIG_HOME` and `XDG_CACHE_HOME`. + +Using a field without a method, as in `if envs.FASTVIDEO_FA4:`, raises `TypeError`. + +## Add a variable + +1. Declare the variable in the matching section of `fastvideo/envs.py`: + + ```python + FASTVIDEO_DEBUG_MY_STAGE = EnvBool(False, category="debug", doc="Log the inputs of MyStage.") + ``` + + The category is one of the values in `envs.CATEGORIES`. + +2. Read the variable inside a function: + + ```python + import fastvideo.envs as envs + + def forward(self, batch): + if envs.FASTVIDEO_DEBUG_MY_STAGE.get(): + logger.info("MyStage inputs: %s", batch.keys()) + ``` + +3. Regenerate the table at the end of this page: + + ```bash + python fastvideo/tests/contract/test_env_policy.py + ``` + +4. Run the contract test: + + ```bash + pytest fastvideo/tests/contract/test_env_policy.py + ``` + +In a test, change the value with `override`, which restores the previous value on exit: + +```python +with envs.FASTVIDEO_DEBUG_MY_STAGE.override(True): + run_stage() +``` + +## What the contract test checks + +The test parses every Python file under `fastvideo/`, including `fastvideo/tests/`, with Python's `ast` module. It +skips `fastvideo/third_party/`, which is copied from upstream projects, and the registry `fastvideo/envs.py`. It does +not check `apps/`, `examples/`, `scripts/`, `fastvideo-kernel/`, or `docs/`. + +It reports each violation as `: `: + +| Kind | Code that triggers it | Fix | +| ------------------ | ------------------------------------------------------------ | -------------------------------------------- | +| `read` | `os.getenv`, `os.environ.get`, `os.environ[...]`, or | Register the variable and call | +| | `"NAME" in os.environ` with a name outside the allowlist, or | `envs.NAME.get()`. For a variable that | +| | with a name built at runtime (``) | another tool owns, add it to | +| | | `EXTERNAL_ALLOWLIST` with a reason. | +| `write` | `os.environ[...] = ...`, `setdefault`, `pop`, `del`, | Pass an argument instead. In tests, use | +| | `os.putenv`, `os.unsetenv`, `monkeypatch.setenv`/`delenv` | `envs.NAME.override()`. | +| `whole-environ` | `os.environ.copy()`, `dict(os.environ)`, iteration, | Read the specific variables that the code | +| | `mock.patch.dict(os.environ, ...)`, `os.environ.update` | needs. | +| `bare-field` | A registry field used without calling one of its methods, | Call `envs.NAME.get()`. | +| | as in `envs.NAME == "auto"` or `getter = envs.NAME.get` | | +| `import-time-read` | `envs.NAME.get()` outside a function | Move the read into the function that uses | +| | | the value. | +| `prefix` | A registry entry without the `FASTVIDEO_` prefix | Rename the variable and keep the old name as | +| | | a deprecated alias. | +| `unread` | A registry entry that no code reads with `get()` or | Delete the variable and add it to the | +| | `is_set()` | deprecation table. | + +The test recognizes `os` imported under another name and `from os import environ, getenv`. Code that reaches the +environment through `importlib` or `getattr(os, "environ")` is left to code review. + +The test also checks that every registry entry has a category from `envs.CATEGORIES` and a description, and that the +table at the end of this page matches the registry. + +**Known violations.** `KNOWN_VIOLATIONS` in the contract test lists the violations that existed when the policy was +introduced. The list only shrinks. A violation that is not in the list fails the test. A listed violation that no +longer exists also fails the test, so the fixing pull request deletes its entry. + +## Registered variables + + +| Variable | Type | Default | Category | Description | +| ---------------------------------------------- | ------------------------------ | --------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `FASTVIDEO_TARGET_DEVICE` | str | `cuda` | build | Target device of FastVideo: cuda, rocm, neuron, cpu, or openvino. | +| `MAX_JOBS` | str | unset | build | Maximum number of parallel compilation jobs. Defaults to the number of CPUs. | +| `NVCC_THREADS` | str | unset | build | Number of nvcc threads. When set, MAX_JOBS is reduced to avoid oversubscribing the CPU. | +| `FASTVIDEO_USE_PRECOMPILED` | bool | `0` | build | Use precompiled binaries (\*.so). | +| `CMAKE_BUILD_TYPE` | str | unset | build | CMake build type: Debug, Release, or RelWithDebInfo. | +| `VERBOSE` | bool | `0` | build | Print verbose logs during installation. | +| `FASTVIDEO_CONFIG_ROOT` | path | computed | path | Root directory for FastVideo configuration files, at runtime and at installation. Defaults to ~/.config/fastvideo, or $XDG_CONFIG_HOME/fastvideo when XDG_CONFIG_HOME is set. | +| `FASTVIDEO_CACHE_ROOT` | path | computed | path | Root directory for FastVideo cache files. Defaults to ~/.cache/fastvideo, or $XDG_CACHE_HOME/fastvideo when XDG_CACHE_HOME is set. | +| `FASTVIDEO_HOST_IP` | str | `""` | distributed | IP address of this node when the node has several network interfaces. Set it on each node for multi-node inference. | +| `FASTVIDEO_LOOPBACK_IP` | str | `""` | distributed | Loopback IP address to use instead of the detected one. | +| `FASTVIDEO_RAY_PER_WORKER_GPUS` | float | `1.0` | distributed | GPUs per Ray worker. A fraction lets Ray schedule several actors on one GPU, so other actors can share the GPUs with FastVideo. | +| `FASTVIDEO_RINGBUFFER_WARNING_INTERVAL` | int | `60` | distributed | Seconds between warnings while the ring buffer is full. | +| `FASTVIDEO_NCCL_SO_PATH` | str | unset | distributed | Path to the NCCL library file. Needed because the nccl>=2.19 that PyTorch ships has a bug (https://github.com/NVIDIA/nccl/issues/1234). | +| `HCCL_SO_PATH` | str | unset | distributed | Path to the HCCL library file on Ascend NPUs. | +| `FASTVIDEO_ENGINE_ITERATION_TIMEOUT_S` | int | `60` | distributed | Timeout in seconds for each engine iteration. | +| `FASTVIDEO_WORKER_MULTIPROC_METHOD` | one of spawn, fork, forkserver | `spawn` | distributed | Multiprocessing start method for worker processes. | +| `FASTVIDEO_ULYSSES_A2A` | one of off, auto | `off` | distributed | Sequence-parallel all-to-all backend. off uses the NCCL path in DistributedAutograd.AllToAll4D. auto uses the fused NVLink kernel when the group is a load-store accessible mesh of 2, 4, 6, or 8 ranks in eager execution, and the NCCL path otherwise. | +| `LD_LIBRARY_PATH` | str | unset | external | Searched for the NCCL library when FASTVIDEO_NCCL_SO_PATH is unset. | +| `LOCAL_RANK` | int | `0` | external | Local rank of the process in a distributed run; selects the GPU device id. | +| `CUDA_VISIBLE_DEVICES` | str | unset | external | Visible devices in a distributed run. | +| `FASTVIDEO_CONFIGURE_LOGGING` | bool | `1` | logging | Configure logging at import. When true, FastVideo uses its default logging configuration or the file in FASTVIDEO_LOGGING_CONFIG_PATH. | +| `FASTVIDEO_LOGGING_CONFIG_PATH` | str | unset | logging | Path to a JSON logging configuration file. | +| `FASTVIDEO_LOGGING_LEVEL` | str | `INFO` | logging | Default logging level. | +| `FASTVIDEO_LOGGING_PREFIX` | str | `""` | logging | Prefix prepended to every log message. | +| `FASTVIDEO_STAGE_LOGGING` | bool | `0` | logging | Log the time that each pipeline stage takes. | +| `FASTVIDEO_ATTENTION_BACKEND` | str | unset | attention | Attention backend, as an AttentionBackendEnum name such as TORCH_SDPA, FLASH_ATTN, VIDEO_SPARSE_ATTN, SAGE_ATTN, or SAGE_ATTN_THREE. FastVideoArgs uses it when FastVideoArgs.attention_backend is unset. | +| `FASTVIDEO_FA4` | bool | `0` | attention | The FLASH_ATTN backend uses FlashAttention-4 (flash_attn.cute) instead of FA3 or FA2. | +| `FASTVIDEO_MINIMAX_H3_FA4_PACKED_VARLEN` | bool | `0` | attention | MiniMax-H3 dense DiT self-attention uses the FlashAttention-4 packed-varlen entry point. This changes the floating-point reduction order, so it is an inference-only opt-in. | +| `FASTVIDEO_VSA_SM100A` | bool | `0` | attention | VIDEO_SPARSE_ATTN_H3 sends no-grad tile-64 forwards to the data-center Blackwell (sm_100a) kernel. fastvideo-kernel reads the same variable with the same rule. | +| `FASTVIDEO_INFERENCE_TORCH_COMPILE` | bool | `0` | performance | Compile each DiT transformer block with fullgraph torch.compile at inference. Same as FastVideoArgs.inference_torch_compile=True. | +| `FASTVIDEO_VAE_PARALLEL_DECODE` | bool | `0` | performance | MiniMax-H3 VAE decode splits its temporal chunks across the sequence-parallel ranks instead of running serially on the output rank. Same as FastVideoArgs.vae_parallel_decode=True. | +| `FASTVIDEO_VAE_PARALLEL_ENCODE` | bool | `0` | performance | MiniMax-H3 reference-video VAE encode splits its temporal chunks across the sequence-parallel ranks. Same as FastVideoArgs.vae_parallel_encode=True. | +| `FASTVIDEO_VAE_PARALLEL_DECODE_STRATEGY` | str | unset | performance | Collective that moves chunks in parallel VAE decode: gather (used when unset) or all_gather. | +| `FASTVIDEO_MINIMAX_H3_FUSIONS` | str | `""` | performance | MiniMax-H3 inference-only Triton fusions: all, 1, or a comma-separated subset of modulate,qknorm_rope,swiglu. Empty, 0, or none keeps the eager implementation. | +| `FASTVIDEO_TEST_DYNAMO_FULLGRAPH_CAPTURE` | bool | `1` | debug | Enable Dynamo fullgraph capture. | +| `FASTVIDEO_NVTX_PROFILE` | bool | `0` | profiling | Emit NVTX ranges for external profilers such as Nsight Systems. | +| `FASTVIDEO_TORCH_PROFILER_DIR` | path | unset | profiling | Enables the torch profiler and sets the directory for its traces. Must be an absolute path. | +| `FASTVIDEO_TORCH_PROFILER_RECORD_SHAPES` | bool | `0` | profiling | Torch profiler records shapes. | +| `FASTVIDEO_TORCH_PROFILER_WITH_PROFILE_MEMORY` | bool | `0` | profiling | Torch profiler profiles memory. | +| `FASTVIDEO_TORCH_PROFILER_WITH_STACK` | bool | `0` | profiling | Torch profiler captures stacks. Costs about 1.5x runtime and 1.4x trace size. | +| `FASTVIDEO_TORCH_PROFILER_WITH_FLOPS` | bool | `0` | profiling | Torch profiler profiles FLOPs. | +| `FASTVIDEO_TORCH_PROFILE_REGIONS` | str | `""` | profiling | Comma-separated profiler regions to record. The torch profiler requires at least one region. | +| `FASTVIDEO_SERVER_DEV_MODE` | bool | `0` | debug | Run the server in development mode with extra debugging endpoints. | +| `FASTVIDEO_TRACE_FUNCTION` | bool | `0` | debug | Trace function calls. | +| `FASTVIDEO_TRACE_ACTIVATIONS` | bool | `0` | debug | Enable activation trace hooks. | +| `FASTVIDEO_TRACE_LAYERS` | str | `""` | debug | Regex filter for traced module names. Empty means all. | +| `FASTVIDEO_TRACE_STATS` | str | `abs_mean,sum` | debug | Comma-separated activation statistics dumped for each output tensor. | +| `FASTVIDEO_TRACE_OUTPUT` | str | `/tmp/fv_trace_.jsonl` | debug | JSONL path for activation traces. The literal <pid> is replaced at runtime. | +| `FASTVIDEO_TRACE_STEPS` | str | `""` | debug | Comma-separated denoising step indices. Empty means all. | +| `FASTVIDEO_CFG_GATE_STEP` | float | `1.0` | sampling | CFG gating fraction in [0, 1]. Steps before len(timesteps) \* X run the conditional and unconditional forwards; later steps reuse the cached difference. 1.0 disables gating. | + diff --git a/fastvideo-kernel/python/fastvideo_kernel/block_sparse_attn.py b/fastvideo-kernel/python/fastvideo_kernel/block_sparse_attn.py index cb5127d74..0b2c56df1 100644 --- a/fastvideo-kernel/python/fastvideo_kernel/block_sparse_attn.py +++ b/fastvideo-kernel/python/fastvideo_kernel/block_sparse_attn.py @@ -60,7 +60,23 @@ def _force_sm100a() -> bool: metadata raises because Triton has no compatible fallback. ``FASTVIDEO_VSA_TRITON`` still wins. """ - return os.environ.get("FASTVIDEO_VSA_SM100A", "0") == "1" + return _env_bool("FASTVIDEO_VSA_SM100A") + + +def _env_bool(name: str) -> bool: + """Parse a boolean environment variable with the rule of ``fastvideo.envs.EnvBool``. + + fastvideo-kernel cannot import fastvideo, so the rule is repeated here to keep + the kernel and the package in agreement: 1, true, yes, on are true; 0, false, + no, off, and the empty string are false; case-insensitive; anything else raises. + """ + value = os.environ.get(name, "0").strip().lower() + if value in ("1", "true", "yes", "on"): + return True + if value in ("0", "false", "no", "off", ""): + return False + raise ValueError(f"Invalid value {value!r} for {name}: expected 1, true, yes, on, 0, false, no, off, " + "or an empty string") def _sm100a_is_supported(q: torch.Tensor, variable_block_sizes: torch.Tensor) -> bool: diff --git a/fastvideo/attention/backends/video_sparse_attn_h3.py b/fastvideo/attention/backends/video_sparse_attn_h3.py index bcc5091bc..e37e6d5a1 100644 --- a/fastvideo/attention/backends/video_sparse_attn_h3.py +++ b/fastvideo/attention/backends/video_sparse_attn_h3.py @@ -45,7 +45,6 @@ logs one warning and falls back. import functools import math -import os from dataclasses import dataclass from typing import Any @@ -69,6 +68,7 @@ try: except ImportError: _sm100a = None +import fastvideo.envs as envs from fastvideo.attention.backends.abstract import (AttentionBackend, AttentionImpl, AttentionMetadata, AttentionMetadataBuilder, layer_idx_from_prefix) from fastvideo.attention.backends.video_sparse_attn import (compute_topk, construct_variable_block_sizes, @@ -469,7 +469,7 @@ class MiniMaxH3VSAImpl(AttentionImpl): """ if self._compile_layer_idx is None: self.prepare_for_compile(device) - requested = os.environ.get(VSA_SM100A_ENV, "0") == "1" + requested = envs.FASTVIDEO_VSA_SM100A.get() enabled = False reason = None if requested else f"{VSA_SM100A_ENV}=1 is required for compile-safe VSA-H3 attention" if requested: @@ -529,7 +529,7 @@ class MiniMaxH3VSAImpl(AttentionImpl): # Training/generic compile keeps the long-standing Triton route. sm100a_requested = False else: - sm100a_requested = os.environ.get(VSA_SM100A_ENV, "0") == "1" + sm100a_requested = envs.FASTVIDEO_VSA_SM100A.get() needs_sm100a_pair = (attn_metadata.tile_elems == 64 and n_tiles % 2 != 0 and not grad_mode and sm100a_requested) kernel_tiles = n_tiles + int(needs_sm100a_pair) target_shape = (x.shape[0], kernel_tiles * attn_metadata.tile_elems, x.shape[-2], x.shape[-1]) @@ -702,7 +702,7 @@ class MiniMaxH3VSAImpl(AttentionImpl): "VSA-H3 regional fullgraph compile requires the prepared sm_100a BF16/head-128 route " "on a supported device; disable inference_torch_compile for this request.") use_sm100a = True - elif not compiling and os.environ.get(VSA_SM100A_ENV, "0") == "1": + elif not compiling and envs.FASTVIDEO_VSA_SM100A.get(): reason = _sm100a_unavailable_reason(_sm100a, q_bhsd, sm100a_variable_block_sizes, grad_mode) if reason is None and map_to_index is None: reason = "fastvideo_kernel.triton_kernels.index (map_to_index) is not importable" diff --git a/fastvideo/attention/selector.py b/fastvideo/attention/selector.py index 03df0b499..302dd4683 100644 --- a/fastvideo/attention/selector.py +++ b/fastvideo/attention/selector.py @@ -208,7 +208,7 @@ def effective_attention_backend(config: object) -> AttentionBackendEnum | None: scope = _SCOPE.get() if scope is not None and (scope.backend is not None or not scope.consult_env): return scope.backend - env_backend = envs.FASTVIDEO_ATTENTION_BACKEND + env_backend = envs.FASTVIDEO_ATTENTION_BACKEND.get() return None if env_backend is None else backend_name_to_enum(env_backend) @@ -249,11 +249,11 @@ def get_attn_backend( if scope is not None: requested = scope.backend component = scope.component - env_backend = envs.FASTVIDEO_ATTENTION_BACKEND if scope.consult_env else None + env_backend = envs.FASTVIDEO_ATTENTION_BACKEND.get() if scope.consult_env else None else: requested = None component = None - env_backend = envs.FASTVIDEO_ATTENTION_BACKEND + env_backend = envs.FASTVIDEO_ATTENTION_BACKEND.get() # The active device is a real selection input, not bookkeeping: the # platform's backend resolution runs capability probes against the # *current* device (e.g. AttnQatInferBackend's per-arch capability sets diff --git a/fastvideo/attention/utils/flash_attn_default.py b/fastvideo/attention/utils/flash_attn_default.py index 3984f68f8..ee50cd8bb 100644 --- a/fastvideo/attention/utils/flash_attn_default.py +++ b/fastvideo/attention/utils/flash_attn_default.py @@ -44,7 +44,7 @@ logger = init_logger(__name__) # kernels JIT-compile per shape family and can fail at runtime on some # arch/shape combinations, so it is never auto-selected just because it is # installed. -if envs.FASTVIDEO_FA4: +if envs.FASTVIDEO_FA4.get(): try: from fastvideo.attention.utils.flash_attn_cute import flash_attn_func except ImportError as e: diff --git a/fastvideo/attention/utils/flash_attn_no_pad.py b/fastvideo/attention/utils/flash_attn_no_pad.py index 52454fd3d..681033f1d 100644 --- a/fastvideo/attention/utils/flash_attn_no_pad.py +++ b/fastvideo/attention/utils/flash_attn_no_pad.py @@ -25,7 +25,7 @@ from fastvideo import envs def _resolve_flash_attn_varlen_func() -> tuple[Any, str]: - if envs.FASTVIDEO_FA4: + if envs.FASTVIDEO_FA4.get(): # FA4 cute is explicit opt-in (see fastvideo/attention/backends/ # flash_attn.py); with FASTVIDEO_FA4=1 an unimportable FA4 build must # fail loudly here rather than fall through to FA3/FA2. RuntimeError, diff --git a/fastvideo/distributed/device_communicators/ulysses_a2a.py b/fastvideo/distributed/device_communicators/ulysses_a2a.py index 2e41a2a1c..ffbd94b92 100644 --- a/fastvideo/distributed/device_communicators/ulysses_a2a.py +++ b/fastvideo/distributed/device_communicators/ulysses_a2a.py @@ -36,7 +36,7 @@ _MODE_FROM_DIMS = {(2, 1): 0, (1, 2): 1} def is_enabled() -> bool: """Whether the fused path is opted in via FASTVIDEO_ULYSSES_A2A.""" - return envs.FASTVIDEO_ULYSSES_A2A == "auto" + return envs.FASTVIDEO_ULYSSES_A2A.get() == "auto" class _FusedUlyssesA2A(torch.autograd.Function): diff --git a/fastvideo/distributed/parallel_state.py b/fastvideo/distributed/parallel_state.py index 5cba91c37..49b2e4f34 100644 --- a/fastvideo/distributed/parallel_state.py +++ b/fastvideo/distributed/parallel_state.py @@ -759,7 +759,7 @@ def init_distributed_environment( if local_rank == -1: # local rank not set, this usually happens in single-node # setting, where we can use rank as local rank - local_rank = envs.LOCAL_RANK if distributed_init_method == "env://" else rank + local_rank = envs.LOCAL_RANK.get() if distributed_init_method == "env://" else rank global _WORLD if _WORLD is None: ranks = list(range(torch.distributed.get_world_size())) @@ -884,9 +884,9 @@ def get_local_torch_device() -> torch.device: """Return the torch device for the current rank.""" from fastvideo.platforms import current_platform if current_platform.is_npu(): - device = torch.device(f"npu:{envs.LOCAL_RANK}") + device = torch.device(f"npu:{envs.LOCAL_RANK.get()}") elif current_platform.is_cuda_alike() or current_platform.is_cuda(): - device = torch.device(f"cuda:{envs.LOCAL_RANK}") + device = torch.device(f"cuda:{envs.LOCAL_RANK.get()}") else: device = torch.device("mps") return device diff --git a/fastvideo/envs.py b/fastvideo/envs.py index 86abc1bcd..bfa8cd809 100644 --- a/fastvideo/envs.py +++ b/fastvideo/envs.py @@ -1,56 +1,160 @@ # SPDX-License-Identifier: Apache-2.0 # Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/envs.py +"""Registry of the environment variables that FastVideo reads. + +Every FastVideo-owned environment variable is declared here once, as a typed +field with a default, a category, and a description. Code reads a variable +with ``envs.NAME.get()`` inside a function, writes it with ``envs.NAME.set()``, +and tests change it temporarily with ``envs.NAME.override()``. Each field type +has one parsing rule, and a value that the rule rejects raises ``EnvVarError``. + +The policy for environment variables is in ``docs/contributing/env_vars.md``, +and ``fastvideo/tests/contract/test_env_policy.py`` enforces it. +""" import os -from collections.abc import Callable -from typing import TYPE_CHECKING, Any +from collections.abc import Callable, Iterator +from contextlib import contextmanager +from typing import Generic, TypeVar -if TYPE_CHECKING: - FASTVIDEO_RINGBUFFER_WARNING_INTERVAL: int = 60 - FASTVIDEO_NCCL_SO_PATH: str | None = None - LD_LIBRARY_PATH: str | None = None - LOCAL_RANK: int = 0 - CUDA_VISIBLE_DEVICES: str | None = None - FASTVIDEO_CACHE_ROOT: str = os.path.expanduser("~/.cache/fastvideo") - FASTVIDEO_CONFIG_ROOT: str = os.path.expanduser("~/.config/fastvideo") - FASTVIDEO_CONFIGURE_LOGGING: int = 1 - FASTVIDEO_RAY_PER_WORKER_GPUS: float = 1.0 - FASTVIDEO_LOGGING_LEVEL: str = "INFO" - FASTVIDEO_LOGGING_PREFIX: str = "" - FASTVIDEO_LOGGING_CONFIG_PATH: str | None = None - FASTVIDEO_TRACE_FUNCTION: int = 0 - FASTVIDEO_ATTENTION_BACKEND: str | None = None - FASTVIDEO_FA4: bool = False - FASTVIDEO_MINIMAX_H3_FA4_PACKED_VARLEN: bool = False - FASTVIDEO_INFERENCE_TORCH_COMPILE: bool = False - FASTVIDEO_MINIMAX_H3_FUSIONS: str = "" - FASTVIDEO_VAE_PARALLEL_DECODE: bool = False - FASTVIDEO_VAE_PARALLEL_ENCODE: bool = False - FASTVIDEO_VAE_PARALLEL_DECODE_STRATEGY: str | None = None - FASTVIDEO_ULYSSES_A2A: str = "off" - FASTVIDEO_WORKER_MULTIPROC_METHOD: str = "spawn" - FASTVIDEO_TARGET_DEVICE: str = "cuda" - MAX_JOBS: str | None = None - NVCC_THREADS: str | None = None - CMAKE_BUILD_TYPE: str | None = None - VERBOSE: bool = False - FASTVIDEO_NVTX_PROFILE: bool = False - FASTVIDEO_TORCH_PROFILER_DIR: str | None = None - FASTVIDEO_TORCH_PROFILER_RECORD_SHAPES: bool = False - FASTVIDEO_TORCH_PROFILER_WITH_PROFILE_MEMORY: bool = False - FASTVIDEO_TORCH_PROFILER_WITH_STACK: bool = False - FASTVIDEO_TORCH_PROFILER_WITH_FLOPS: bool = False - FASTVIDEO_TORCH_PROFILE_REGIONS: str = "" - FASTVIDEO_TRACE_ACTIVATIONS: bool = False - FASTVIDEO_TRACE_LAYERS: str = "" - FASTVIDEO_TRACE_STATS: str = "abs_mean,sum" - FASTVIDEO_TRACE_OUTPUT: str = "/tmp/fv_trace_.jsonl" - FASTVIDEO_TRACE_STEPS: str = "" - FASTVIDEO_SERVER_DEV_MODE: bool = False - FASTVIDEO_STAGE_LOGGING: bool = False - FASTVIDEO_CFG_GATE_STEP: float = 1.0 - FASTVIDEO_HOST_IP: str = "" - FASTVIDEO_LOOPBACK_IP: str = "" +T = TypeVar("T") +# String fields accept a string default or None (unset). +S = TypeVar("S", str, str | None) + +POLICY_DOC = "docs/contributing/env_vars.md" + +# Allowed values of EnvField.category. +CATEGORIES = ("build", "path", "distributed", "external", "logging", "attention", "performance", "profiling", "debug", + "sampling") + +_TRUE_VALUES = frozenset({"1", "true", "yes", "on"}) +_FALSE_VALUES = frozenset({"0", "false", "no", "off", ""}) + + +class EnvVarError(ValueError): + """An environment variable holds a value that its registered type rejects.""" + + +class EnvField(Generic[T]): + """One registered environment variable: its type, default, category, and description. + + ``default`` is either the value itself or a zero-argument function that + computes it on each read while the variable is unset. + """ + + type_name = "" + + def __init__(self, default: T | Callable[[], T], *, category: str, doc: str) -> None: + self.name = "" # Set by _register_fields() from the module attribute name. + self.default = default + self.category = category + self.doc = doc + + def parse(self, raw: str) -> T: + raise NotImplementedError + + def format(self, value: T) -> str: + return str(value) + + def get(self) -> T: + """Return the parsed value, or the default when the variable is unset.""" + raw = os.environ.get(self.name) + if raw is None: + return self.default() if callable(self.default) else self.default + try: + return self.parse(raw) + except ValueError as exc: + raise EnvVarError(f"Invalid value {raw!r} for {self.name}: {exc}. See {POLICY_DOC}.") from None + + def is_set(self) -> bool: + return self.name in os.environ + + def set(self, value: T) -> None: + os.environ[self.name] = self.format(value) + + def clear(self) -> None: + os.environ.pop(self.name, None) + + @contextmanager + def override(self, value: T | None) -> Iterator[None]: + """Set the variable, or unset it when ``value`` is None, and restore the previous value on exit.""" + previous = os.environ.get(self.name) + if value is None: + self.clear() + else: + self.set(value) + try: + yield + finally: + if previous is None: + self.clear() + else: + os.environ[self.name] = previous + + def __bool__(self) -> bool: + raise TypeError(f"Use envs.{self.name}.get() to read {self.name}.") + + +class EnvBool(EnvField[bool]): + """True for 1, true, yes, on; false for 0, false, no, off, and the empty string; case-insensitive.""" + + type_name = "bool" + + def parse(self, raw: str) -> bool: + value = raw.strip().lower() + if value in _TRUE_VALUES: + return True + if value in _FALSE_VALUES: + return False + raise ValueError("expected 1, true, yes, on, 0, false, no, off, or an empty string") + + def format(self, value: bool) -> str: + return "1" if value else "0" + + +class EnvInt(EnvField[int]): + type_name = "int" + + def parse(self, raw: str) -> int: + return int(raw) + + +class EnvFloat(EnvField[float]): + type_name = "float" + + def parse(self, raw: str) -> float: + return float(raw) + + +class EnvStr(EnvField[S]): + type_name = "str" + + def parse(self, raw: str) -> S: + return raw + + +class EnvPath(EnvField[S]): + """A filesystem path; a leading ``~`` is expanded.""" + + type_name = "path" + + def parse(self, raw: str) -> S: + return os.path.expanduser(raw) + + +class EnvChoice(EnvField[str]): + """One of ``choices``; the value is stripped and lower-cased before the check.""" + + def __init__(self, default: str, *, choices: tuple[str, ...], category: str, doc: str) -> None: + super().__init__(default, category=category, doc=doc) + self.choices = choices + self.type_name = "one of " + ", ".join(choices) + + def parse(self, raw: str) -> str: + value = raw.strip().lower() + if value not in self.choices: + raise ValueError(f"expected one of {', '.join(self.choices)}") + return value def get_default_cache_root() -> str: @@ -67,318 +171,228 @@ def get_default_config_root() -> str: ) -def maybe_convert_int(value: str | None) -> int | None: - if value is None: - return None - return int(value) +# ================== Installation ================== + +FASTVIDEO_TARGET_DEVICE = EnvStr("cuda", + category="build", + doc="Target device of FastVideo: cuda, rocm, neuron, cpu, or openvino.") +MAX_JOBS = EnvStr(None, + category="build", + doc="Maximum number of parallel compilation jobs. Defaults to the number of CPUs.") +NVCC_THREADS = EnvStr(None, + category="build", + doc="Number of nvcc threads. When set, MAX_JOBS is reduced to avoid oversubscribing the CPU.") +FASTVIDEO_USE_PRECOMPILED = EnvBool(False, category="build", doc="Use precompiled binaries (*.so).") +CMAKE_BUILD_TYPE = EnvStr(None, category="build", doc="CMake build type: Debug, Release, or RelWithDebInfo.") +VERBOSE = EnvBool(False, category="build", doc="Print verbose logs during installation.") + +# ================== Paths ================== + +FASTVIDEO_CONFIG_ROOT = EnvPath( + lambda: os.path.expanduser(os.path.join(get_default_config_root(), "fastvideo")), + category="path", + doc="Root directory for FastVideo configuration files, at runtime and at installation. " + "Defaults to ~/.config/fastvideo, or $XDG_CONFIG_HOME/fastvideo when XDG_CONFIG_HOME is set.") +FASTVIDEO_CACHE_ROOT = EnvPath( + lambda: os.path.expanduser(os.path.join(get_default_cache_root(), "fastvideo")), + category="path", + doc="Root directory for FastVideo cache files. " + "Defaults to ~/.cache/fastvideo, or $XDG_CACHE_HOME/fastvideo when XDG_CACHE_HOME is set.") + +# ================== Distributed ================== + +FASTVIDEO_HOST_IP = EnvStr( + "", + category="distributed", + doc="IP address of this node when the node has several network interfaces. Set it on each node for multi-node " + "inference.") +FASTVIDEO_LOOPBACK_IP = EnvStr("", + category="distributed", + doc="Loopback IP address to use instead of the detected one.") +FASTVIDEO_RAY_PER_WORKER_GPUS = EnvFloat( + 1.0, + category="distributed", + doc="GPUs per Ray worker. A fraction lets Ray schedule several actors on one GPU, so other actors can share " + "the GPUs with FastVideo.") +FASTVIDEO_RINGBUFFER_WARNING_INTERVAL = EnvInt(60, + category="distributed", + doc="Seconds between warnings while the ring buffer is full.") +FASTVIDEO_NCCL_SO_PATH = EnvStr( + None, + category="distributed", + doc="Path to the NCCL library file. Needed because the nccl>=2.19 that PyTorch ships has a bug " + "(https://github.com/NVIDIA/nccl/issues/1234).") +HCCL_SO_PATH = EnvStr(None, category="distributed", doc="Path to the HCCL library file on Ascend NPUs.") +FASTVIDEO_ENGINE_ITERATION_TIMEOUT_S = EnvInt(60, + category="distributed", + doc="Timeout in seconds for each engine iteration.") +FASTVIDEO_WORKER_MULTIPROC_METHOD = EnvChoice("spawn", + choices=("spawn", "fork", "forkserver"), + category="distributed", + doc="Multiprocessing start method for worker processes.") +FASTVIDEO_ULYSSES_A2A = EnvChoice( + "off", + choices=("off", "auto"), + category="distributed", + doc="Sequence-parallel all-to-all backend. off uses the NCCL path in DistributedAutograd.AllToAll4D. auto uses " + "the fused NVLink kernel when the group is a load-store accessible mesh of 2, 4, 6, or 8 ranks in eager " + "execution, and the NCCL path otherwise.") + +# ================== External variables ================== +# Variables that other tools set. They stay registered so that Ray copies them +# to its workers until the external-variable allowlist replaces them. + +LD_LIBRARY_PATH = EnvStr(None, + category="external", + doc="Searched for the NCCL library when FASTVIDEO_NCCL_SO_PATH is unset.") +LOCAL_RANK = EnvInt(0, + category="external", + doc="Local rank of the process in a distributed run; selects the GPU device id.") +CUDA_VISIBLE_DEVICES = EnvStr(None, category="external", doc="Visible devices in a distributed run.") + +# ================== Logging ================== + +FASTVIDEO_CONFIGURE_LOGGING = EnvBool( + True, + category="logging", + doc="Configure logging at import. When true, FastVideo uses its default logging configuration or the file in " + "FASTVIDEO_LOGGING_CONFIG_PATH.") +FASTVIDEO_LOGGING_CONFIG_PATH = EnvStr(None, category="logging", doc="Path to a JSON logging configuration file.") +FASTVIDEO_LOGGING_LEVEL = EnvStr("INFO", category="logging", doc="Default logging level.") +FASTVIDEO_LOGGING_PREFIX = EnvStr("", category="logging", doc="Prefix prepended to every log message.") +FASTVIDEO_STAGE_LOGGING = EnvBool(False, category="logging", doc="Log the time that each pipeline stage takes.") + +# ================== Attention ================== + +FASTVIDEO_ATTENTION_BACKEND = EnvStr( + None, + category="attention", + doc="Attention backend, as an AttentionBackendEnum name such as TORCH_SDPA, FLASH_ATTN, VIDEO_SPARSE_ATTN, " + "SAGE_ATTN, or SAGE_ATTN_THREE. FastVideoArgs uses it when FastVideoArgs.attention_backend is unset.") +# FA4 is opt-in and never auto-selected just because it is installed. Below +# sm90, grad-enabled and GQA calls are routed to FA2 (FA4's backward asserts +# sm90+ and its pack_gqa fails to JIT there). +FASTVIDEO_FA4 = EnvBool(False, + category="attention", + doc="The FLASH_ATTN backend uses FlashAttention-4 (flash_attn.cute) instead of FA3 or FA2.") +FASTVIDEO_MINIMAX_H3_FA4_PACKED_VARLEN = EnvBool( + False, + category="attention", + doc="MiniMax-H3 dense DiT self-attention uses the FlashAttention-4 packed-varlen entry point. This changes the " + "floating-point reduction order, so it is an inference-only opt-in.") +FASTVIDEO_VSA_SM100A = EnvBool( + False, + category="attention", + doc="VIDEO_SPARSE_ATTN_H3 sends no-grad tile-64 forwards to the data-center Blackwell (sm_100a) kernel. " + "fastvideo-kernel reads the same variable with the same rule.") + +# ================== Performance ================== + +# Non-fullgraph-traceable attention backends such as VSA degrade to eager with +# one warning; see _regional_compile_unsupported_reason in +# fastvideo/models/loader/fsdp_load.py. +FASTVIDEO_INFERENCE_TORCH_COMPILE = EnvBool( + False, + category="performance", + doc="Compile each DiT transformer block with fullgraph torch.compile at inference. Same as " + "FastVideoArgs.inference_torch_compile=True.") +FASTVIDEO_VAE_PARALLEL_DECODE = EnvBool( + False, + category="performance", + doc="MiniMax-H3 VAE decode splits its temporal chunks across the sequence-parallel ranks instead of running " + "serially on the output rank. Same as FastVideoArgs.vae_parallel_decode=True.") +FASTVIDEO_VAE_PARALLEL_ENCODE = EnvBool( + False, + category="performance", + doc="MiniMax-H3 reference-video VAE encode splits its temporal chunks across the sequence-parallel ranks. " + "Same as FastVideoArgs.vae_parallel_encode=True.") +FASTVIDEO_VAE_PARALLEL_DECODE_STRATEGY = EnvStr( + None, + category="performance", + doc="Collective that moves chunks in parallel VAE decode: gather (used when unset) or all_gather.") +# Adapted from the NVlabs/Sana Sol-Engine implementation. +FASTVIDEO_MINIMAX_H3_FUSIONS = EnvStr( + "", + category="performance", + doc="MiniMax-H3 inference-only Triton fusions: all, 1, or a comma-separated subset of " + "modulate,qknorm_rope,swiglu. Empty, 0, or none keeps the eager implementation.") +FASTVIDEO_TEST_DYNAMO_FULLGRAPH_CAPTURE = EnvBool(True, category="debug", doc="Enable Dynamo fullgraph capture.") + +# ================== Profiling ================== + +FASTVIDEO_NVTX_PROFILE = EnvBool(False, + category="profiling", + doc="Emit NVTX ranges for external profilers such as Nsight Systems.") +FASTVIDEO_TORCH_PROFILER_DIR = EnvPath( + None, + category="profiling", + doc="Enables the torch profiler and sets the directory for its traces. Must be an absolute path.") +FASTVIDEO_TORCH_PROFILER_RECORD_SHAPES = EnvBool(False, category="profiling", doc="Torch profiler records shapes.") +FASTVIDEO_TORCH_PROFILER_WITH_PROFILE_MEMORY = EnvBool(False, + category="profiling", + doc="Torch profiler profiles memory.") +FASTVIDEO_TORCH_PROFILER_WITH_STACK = EnvBool( + False, category="profiling", doc="Torch profiler captures stacks. Costs about 1.5x runtime and 1.4x trace size.") +FASTVIDEO_TORCH_PROFILER_WITH_FLOPS = EnvBool(False, category="profiling", doc="Torch profiler profiles FLOPs.") +FASTVIDEO_TORCH_PROFILE_REGIONS = EnvStr( + "", + category="profiling", + doc="Comma-separated profiler regions to record. The torch profiler requires at least one region.") + +# ================== Debug ================== + +FASTVIDEO_SERVER_DEV_MODE = EnvBool(False, + category="debug", + doc="Run the server in development mode with extra debugging endpoints.") +FASTVIDEO_TRACE_FUNCTION = EnvBool(False, category="debug", doc="Trace function calls.") +FASTVIDEO_TRACE_ACTIVATIONS = EnvBool(False, category="debug", doc="Enable activation trace hooks.") +FASTVIDEO_TRACE_LAYERS = EnvStr("", category="debug", doc="Regex filter for traced module names. Empty means all.") +FASTVIDEO_TRACE_STATS = EnvStr("abs_mean,sum", + category="debug", + doc="Comma-separated activation statistics dumped for each output tensor.") +FASTVIDEO_TRACE_OUTPUT = EnvStr("/tmp/fv_trace_.jsonl", + category="debug", + doc="JSONL path for activation traces. The literal is replaced at runtime.") +FASTVIDEO_TRACE_STEPS = EnvStr("", category="debug", doc="Comma-separated denoising step indices. Empty means all.") + +# ================== Sampling ================== + +# CFG gating fraction for stale-uncond reuse (Adaptive Guidance / LinearAG +# variant — Castillo et al. 2023, arXiv:2312.12487). Float in [0, 1]. +# Interpretation: for step index `i < len(timesteps) * X`, run both +# cond and uncond forwards and refresh delta_cached = cond - uncond. +# Once `i >= len(timesteps) * X`, skip the uncond forward and reuse +# the cached delta: noise_pred = cond + (guidance_scale - 1) * delta. +# +# Edge cases: +# 1.0 (default) : disables gating; identical to baseline two-pass CFG. +# 0.5 : run uncond for the first half of steps, reuse delta +# for the second half (~25% inference time saved on +# bandwidth-bound SP setups). +# 0.0 : step 0 still computes uncond fresh (cache is empty +# at start) — all subsequent steps reuse the step-0 +# delta. This is the most aggressive setting; does +# NOT mean "no uncond forward ever." +# +# Caveats: +# - Algorithmically approximate; not bit-exact vs baseline CFG. +# Validate per-pipeline with SSIM / VBench before lowering below 1.0. +# - Interaction with `guidance_rescale > 0` is unvalidated; the +# denoising stage logs a warning when both are active. +# - Wan2.2 high/low-noise expert switch invalidates the cache. +FASTVIDEO_CFG_GATE_STEP = EnvFloat( + 1.0, + category="sampling", + doc="CFG gating fraction in [0, 1]. Steps before len(timesteps) * X run the conditional and unconditional " + "forwards; later steps reuse the cached difference. 1.0 disables gating.") -# The begin-* and end* here are used by the documentation generator -# to extract the used env vars. - -# begin-env-vars-definition - -environment_variables: dict[str, Callable[[], Any]] = { - - # ================== Installation Time Env Vars ================== - - # Target device of FastVideo, supporting [cuda (by default), - # rocm, neuron, cpu, openvino] - "FASTVIDEO_TARGET_DEVICE": - lambda: os.getenv("FASTVIDEO_TARGET_DEVICE", "cuda"), - - # Maximum number of compilation jobs to run in parallel. - # By default this is the number of CPUs - "MAX_JOBS": - lambda: os.getenv("MAX_JOBS", None), - - # Number of threads to use for nvcc - # By default this is 1. - # If set, `MAX_JOBS` will be reduced to avoid oversubscribing the CPU. - "NVCC_THREADS": - lambda: os.getenv("NVCC_THREADS", None), - - # If set, fastvideo will use precompiled binaries (*.so) - "FASTVIDEO_USE_PRECOMPILED": - lambda: bool(os.environ.get("FASTVIDEO_USE_PRECOMPILED")) or bool( - os.environ.get("FASTVIDEO_PRECOMPILED_WHEEL_LOCATION")), - - # CMake build type - # If not set, defaults to "Debug" or "RelWithDebInfo" - # Available options: "Debug", "Release", "RelWithDebInfo" - "CMAKE_BUILD_TYPE": - lambda: os.getenv("CMAKE_BUILD_TYPE"), - - # If set, fastvideo will print verbose logs during installation - "VERBOSE": - lambda: bool(int(os.getenv('VERBOSE', '0'))), - - # Root directory for FASTVIDEO configuration files - # Defaults to `~/.config/fastvideo` unless `XDG_CONFIG_HOME` is set - # Note that this not only affects how fastvideo finds its configuration files - # during runtime, but also affects how fastvideo installs its configuration - # files during **installation**. - "FASTVIDEO_CONFIG_ROOT": - lambda: os.path.expanduser( - os.getenv( - "FASTVIDEO_CONFIG_ROOT", - os.path.join(get_default_config_root(), "fastvideo"), - )), - - # ================== Runtime Env Vars ================== - - # Root directory for FASTVIDEO cache files - # Defaults to `~/.cache/fastvideo` unless `XDG_CACHE_HOME` is set - "FASTVIDEO_CACHE_ROOT": - lambda: os.path.expanduser(os.getenv( - "FASTVIDEO_CACHE_ROOT", - os.path.join(get_default_cache_root(), "fastvideo"), - )), - - # used in distributed environment to determine the ip address - # of the current node, when the node has multiple network interfaces. - # If you are using multi-node inference, you should set this differently - # on each node. - "FASTVIDEO_HOST_IP": - lambda: os.getenv("FASTVIDEO_HOST_IP", ""), - - # Used to force set up loopback IP - "FASTVIDEO_LOOPBACK_IP": - lambda: os.getenv("FASTVIDEO_LOOPBACK_IP", ""), - - # Number of GPUs per worker in Ray, if it is set to be a fraction, - # it allows ray to schedule multiple actors on a single GPU, - # so that users can colocate other actors on the same GPUs as FastVideo. - "FASTVIDEO_RAY_PER_WORKER_GPUS": - lambda: float(os.getenv("FASTVIDEO_RAY_PER_WORKER_GPUS", "1.0")), - - # Interval in seconds to log a warning message when the ring buffer is full - "FASTVIDEO_RINGBUFFER_WARNING_INTERVAL": - lambda: int(os.environ.get("FASTVIDEO_RINGBUFFER_WARNING_INTERVAL", "60")), - - # Path to the NCCL library file. It is needed because nccl>=2.19 brought - # by PyTorch contains a bug: https://github.com/NVIDIA/nccl/issues/1234 - "FASTVIDEO_NCCL_SO_PATH": - lambda: os.environ.get("FASTVIDEO_NCCL_SO_PATH", None), - - # when `FASTVIDEO_NCCL_SO_PATH` is not set, fastvideo will try to find the nccl - # library file in the locations specified by `LD_LIBRARY_PATH` - "LD_LIBRARY_PATH": - lambda: os.environ.get("LD_LIBRARY_PATH", None), - - # Internal flag to enable Dynamo fullgraph capture - "FASTVIDEO_TEST_DYNAMO_FULLGRAPH_CAPTURE": - lambda: bool(os.environ.get("FASTVIDEO_TEST_DYNAMO_FULLGRAPH_CAPTURE", "1") != "0"), - - # local rank of the process in the distributed setting, used to determine - # the GPU device id - "LOCAL_RANK": - lambda: int(os.environ.get("LOCAL_RANK", "0")), - - # used to control the visible devices in the distributed setting - "CUDA_VISIBLE_DEVICES": - lambda: os.environ.get("CUDA_VISIBLE_DEVICES", None), - - # timeout for each iteration in the engine - "FASTVIDEO_ENGINE_ITERATION_TIMEOUT_S": - lambda: int(os.environ.get("FASTVIDEO_ENGINE_ITERATION_TIMEOUT_S", "60")), - - # Logging configuration - # If set to 0, fastvideo will not configure logging - # If set to 1, fastvideo will configure logging using the default configuration - # or the configuration file specified by FASTVIDEO_LOGGING_CONFIG_PATH - "FASTVIDEO_CONFIGURE_LOGGING": - lambda: int(os.getenv("FASTVIDEO_CONFIGURE_LOGGING", "1")), - "FASTVIDEO_LOGGING_CONFIG_PATH": - lambda: os.getenv("FASTVIDEO_LOGGING_CONFIG_PATH"), - - # this is used for configuring the default logging level - "FASTVIDEO_LOGGING_LEVEL": - lambda: os.getenv("FASTVIDEO_LOGGING_LEVEL", "INFO"), - - # if set, FASTVIDEO_LOGGING_PREFIX will be prepended to all log messages - "FASTVIDEO_LOGGING_PREFIX": - lambda: os.getenv("FASTVIDEO_LOGGING_PREFIX", ""), - - # Trace function calls - # If set to 1, fastvideo will trace function calls - # Useful for debugging - "FASTVIDEO_TRACE_FUNCTION": - lambda: int(os.getenv("FASTVIDEO_TRACE_FUNCTION", "0")), - - # Backend for attention computation - # Available options: - # - "TORCH_SDPA": use torch.nn.MultiheadAttention - # - "FLASH_ATTN": use FlashAttention - # - "VIDEO_SPARSE_ATTN": use Video Sparse Attention - # - "SAGE_ATTN": use Sage Attention - # - "SAGE_ATTN_THREE": use Sage Attention 3 - # FLASH_ATTN uses FlashAttention-3/2; to run FlashAttention-4 set - # FASTVIDEO_FA4=1 as well (see below). - "FASTVIDEO_ATTENTION_BACKEND": - lambda: os.getenv("FASTVIDEO_ATTENTION_BACKEND", None), - - # If set (=1), the FLASH_ATTN backend uses FlashAttention-4 - # (flash_attn.cute). FA4 is opt-in and never auto-selected just because it - # is installed. Below sm90, grad-enabled and GQA calls are routed to FA2 - # (FA4's backward asserts sm90+ and its pack_gqa fails to JIT there). - "FASTVIDEO_FA4": - lambda: os.getenv("FASTVIDEO_FA4", "0") != "0", - - # Use FA4's packed-varlen entry point for the long, single-document - # MiniMax-H3 dense DiT self-attention path. This changes floating-point - # reduction order relative to the fixed-length entry point, so it remains - # an explicit inference-only speed/quality opt-in. - "FASTVIDEO_MINIMAX_H3_FA4_PACKED_VARLEN": - lambda: os.getenv("FASTVIDEO_MINIMAX_H3_FA4_PACKED_VARLEN", "0") != "0", - - # If set (=1), enable regional (per-transformer-block) fullgraph - # torch.compile for the DiT at inference — the inference-side counterpart - # of the training regional-compile port of hao-ai-lab/FastVideo#1718. - # Equivalent to FastVideoArgs.inference_torch_compile=True (e.g. via - # PipelineSelection.experimental={"inference_torch_compile": True}). VSA - # and other non-fullgraph-traceable attention backends degrade to eager - # with one warning; see _regional_compile_unsupported_reason in - # fastvideo/models/loader/fsdp_load.py. - "FASTVIDEO_INFERENCE_TORCH_COMPILE": - lambda: os.getenv("FASTVIDEO_INFERENCE_TORCH_COMPILE", "0") != "0", - - # If set (=1), MiniMax-H3 VAE decode (and, with the ENCODE variant, - # reference-video encode) round-robins its temporal chunks across the - # sequence-parallel ranks instead of running serially on the output rank. - # Folded into FastVideoArgs.vae_parallel_decode / vae_parallel_encode at - # construction (parse-once). The STRATEGY variant picks the chunk - # transport collective: "gather" (default) or "all_gather". - "FASTVIDEO_VAE_PARALLEL_DECODE": - lambda: os.getenv("FASTVIDEO_VAE_PARALLEL_DECODE", "0") != "0", - "FASTVIDEO_VAE_PARALLEL_ENCODE": - lambda: os.getenv("FASTVIDEO_VAE_PARALLEL_ENCODE", "0") != "0", - "FASTVIDEO_VAE_PARALLEL_DECODE_STRATEGY": - lambda: os.getenv("FASTVIDEO_VAE_PARALLEL_DECODE_STRATEGY", None), - - # Opt-in MiniMax-H3 inference-only Triton fusions adapted from the - # NVlabs/Sana Sol-Engine implementation. Accepts `all`, `1`, or a - # comma-separated subset of `modulate,qknorm_rope,swiglu`. An empty value - # (the default), `0`, or `none` keeps the eager implementation. - "FASTVIDEO_MINIMAX_H3_FUSIONS": - lambda: os.getenv("FASTVIDEO_MINIMAX_H3_FUSIONS", ""), - - # Sequence-parallel all-to-all backend. - # - "off" (default): the NCCL path in DistributedAutograd.AllToAll4D - # - "auto": fused NVLink kernel when the group is a load-store accessible - # mesh of 2/4/6/8 ranks in eager execution, else the NCCL path. - "FASTVIDEO_ULYSSES_A2A": - lambda: os.getenv("FASTVIDEO_ULYSSES_A2A", "off").strip().lower(), - - # Use dedicated multiprocess context for workers. - "FASTVIDEO_WORKER_MULTIPROC_METHOD": - lambda: os.getenv("FASTVIDEO_WORKER_MULTIPROC_METHOD", "spawn"), - - # Emit lightweight NVTX ranges for external profilers such as Nsight Systems. - "FASTVIDEO_NVTX_PROFILE": - lambda: os.getenv("FASTVIDEO_NVTX_PROFILE", "0") != "0", - - # Enables torch profiler if set. Path to the directory where torch profiler - # traces are saved. Note that it must be an absolute path. - "FASTVIDEO_TORCH_PROFILER_DIR": - lambda: (None if os.getenv("FASTVIDEO_TORCH_PROFILER_DIR", None) is None else os.path.expanduser( - os.getenv("FASTVIDEO_TORCH_PROFILER_DIR", "."))), - - # Enable torch profiler to record shapes if set - # FASTVIDEO_TORCH_PROFILER_RECORD_SHAPES=1. If not set, torch profiler will - # not record shapes. - "FASTVIDEO_TORCH_PROFILER_RECORD_SHAPES": - lambda: bool(os.getenv("FASTVIDEO_TORCH_PROFILER_RECORD_SHAPES", "0") != "0"), - - # Enable torch profiler to profile memory if set - # FASTVIDEO_TORCH_PROFILER_WITH_PROFILE_MEMORY=1. If not set, torch profiler - # will not profile memory. - "FASTVIDEO_TORCH_PROFILER_WITH_PROFILE_MEMORY": - lambda: bool(os.getenv("FASTVIDEO_TORCH_PROFILER_WITH_PROFILE_MEMORY", "0") != "0"), - - # Enable torch profiler stack capture with - # FASTVIDEO_TORCH_PROFILER_WITH_STACK=1. Off by default: stack capture - # costs ~1.5x runtime overhead and ~1.4x trace size. - "FASTVIDEO_TORCH_PROFILER_WITH_STACK": - lambda: bool(os.getenv("FASTVIDEO_TORCH_PROFILER_WITH_STACK", "0") != "0"), - - # Enable torch profiler to profile flops if set - # FASTVIDEO_TORCH_PROFILER_WITH_FLOPS=1. If not set, torch profiler will - # not profile flops. - "FASTVIDEO_TORCH_PROFILER_WITH_FLOPS": - lambda: bool(os.getenv("FASTVIDEO_TORCH_PROFILER_WITH_FLOPS", "0") != "0"), - # Wait steps per profiling cycle (torch.profiler.schedule wait parameter) - # Defaults to 2 if not set. - # Warmup steps per profiling cycle (torch.profiler.schedule warmup parameter) - # Defaults to 1 if not set. - # Active steps per profiling cycle (torch.profiler.schedule active parameter) - # Defaults to 2 if not set. - "FASTVIDEO_TORCH_PROFILE_REGIONS": - lambda: os.getenv("FASTVIDEO_TORCH_PROFILE_REGIONS", ""), - - # Enable activation trace hooks if set. - "FASTVIDEO_TRACE_ACTIVATIONS": - lambda: bool(os.getenv("FASTVIDEO_TRACE_ACTIVATIONS", "0") != "0"), - # Regex filter for traced module names. Empty means all modules. - "FASTVIDEO_TRACE_LAYERS": - lambda: os.getenv("FASTVIDEO_TRACE_LAYERS", ""), - # Comma-separated activation stats to dump for each output tensor. - "FASTVIDEO_TRACE_STATS": - lambda: os.getenv("FASTVIDEO_TRACE_STATS", "abs_mean,sum"), - # JSONL sink path. The literal is replaced at runtime. - "FASTVIDEO_TRACE_OUTPUT": - lambda: os.getenv("FASTVIDEO_TRACE_OUTPUT", "/tmp/fv_trace_.jsonl"), - # Comma-separated denoise step indices. Empty means all steps. - "FASTVIDEO_TRACE_STEPS": - lambda: os.getenv("FASTVIDEO_TRACE_STEPS", ""), - - # If set, fastvideo will run in development mode, which will enable - # some additional endpoints for developing and debugging, - # e.g. `/reset_prefix_cache` - "FASTVIDEO_SERVER_DEV_MODE": - lambda: bool(int(os.getenv("FASTVIDEO_SERVER_DEV_MODE", "0"))), - - # If set, fastvideo will enable stage logging, which will print the time - # taken for each stage - "FASTVIDEO_STAGE_LOGGING": - lambda: bool(int(os.getenv("FASTVIDEO_STAGE_LOGGING", "0"))), - - # CFG gating fraction for stale-uncond reuse (Adaptive Guidance / LinearAG - # variant — Castillo et al. 2023, arXiv:2312.12487). Float in [0, 1]. - # Interpretation: for step index `i < len(timesteps) * X`, run both - # cond and uncond forwards and refresh delta_cached = cond - uncond. - # Once `i >= len(timesteps) * X`, skip the uncond forward and reuse - # the cached delta: noise_pred = cond + (guidance_scale - 1) * delta. - # - # Edge cases: - # 1.0 (default) : disables gating; identical to baseline two-pass CFG. - # 0.5 : run uncond for the first half of steps, reuse delta - # for the second half (~25% inference time saved on - # bandwidth-bound SP setups). - # 0.0 : step 0 still computes uncond fresh (cache is empty - # at start) — all subsequent steps reuse the step-0 - # delta. This is the most aggressive setting; does - # NOT mean "no uncond forward ever." - # - # Caveats: - # - Algorithmically approximate; not bit-exact vs baseline CFG. - # Validate per-pipeline with SSIM / VBench before lowering below 1.0. - # - Interaction with `guidance_rescale > 0` is unvalidated; the - # denoising stage logs a warning when both are active. - # - Wan2.2 high/low-noise expert switch invalidates the cache. - "FASTVIDEO_CFG_GATE_STEP": - lambda: float(os.getenv("FASTVIDEO_CFG_GATE_STEP", "1.0")), -} - -# end-env-vars-definition +def _register_fields() -> dict[str, EnvField]: + """Name each module-level EnvField after its attribute and return the fields by name.""" + fields = {name: value for name, value in globals().items() if isinstance(value, EnvField)} + for name, field in fields.items(): + field.name = name + return fields -def __getattr__(name: str): - # lazy evaluation of environment variables - if name in environment_variables: - return environment_variables[name]() - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - - -def __dir__(): - return list(environment_variables.keys()) +environment_variables: dict[str, EnvField] = _register_fields() diff --git a/fastvideo/eval/models.py b/fastvideo/eval/models.py index 8b549e9af..9adb57b3e 100644 --- a/fastvideo/eval/models.py +++ b/fastvideo/eval/models.py @@ -46,7 +46,7 @@ def get_cache_dir() -> Path: """ return Path(os.environ.get( "FASTVIDEO_EVAL_CACHE", - os.path.join(envs.FASTVIDEO_CACHE_ROOT, "eval"), + os.path.join(envs.FASTVIDEO_CACHE_ROOT.get(), "eval"), )) diff --git a/fastvideo/fastvideo_args.py b/fastvideo/fastvideo_args.py index a648a0253..8cc5a60bf 100644 --- a/fastvideo/fastvideo_args.py +++ b/fastvideo/fastvideo_args.py @@ -343,7 +343,7 @@ class FastVideoArgs: # environment variable is an input read once here, so the loader # only ever consults the typed field. import fastvideo.envs as envs - if envs.FASTVIDEO_INFERENCE_TORCH_COMPILE: + if envs.FASTVIDEO_INFERENCE_TORCH_COMPILE.get(): self.inference_torch_compile = True if self.attention_backend is not None: # Fail fast on typos instead of silently auto-selecting later. @@ -357,7 +357,7 @@ class FastVideoArgs: # and falls through to automatic selection rather than raising. import fastvideo.envs as envs from fastvideo.attention.selector import backend_name_to_enum - env_backend = envs.FASTVIDEO_ATTENTION_BACKEND + env_backend = envs.FASTVIDEO_ATTENTION_BACKEND.get() if env_backend is not None and backend_name_to_enum(env_backend) is not None: self.attention_backend = env_backend self._fold_vae_parallel_env() @@ -371,12 +371,12 @@ class FastVideoArgs: # DEFAULT_DECODE_GATHER_STRATEGY (kept literal here so constructing args # never imports model modules; a unit test pins the two in sync). strategies = ("gather", "all_gather") - if not self.vae_parallel_decode and envs.FASTVIDEO_VAE_PARALLEL_DECODE: + if not self.vae_parallel_decode and envs.FASTVIDEO_VAE_PARALLEL_DECODE.get(): self.vae_parallel_decode = True - if not self.vae_parallel_encode and envs.FASTVIDEO_VAE_PARALLEL_ENCODE: + if not self.vae_parallel_encode and envs.FASTVIDEO_VAE_PARALLEL_ENCODE.get(): self.vae_parallel_encode = True if self.vae_parallel_decode_strategy is None: - self.vae_parallel_decode_strategy = envs.FASTVIDEO_VAE_PARALLEL_DECODE_STRATEGY or "gather" + self.vae_parallel_decode_strategy = envs.FASTVIDEO_VAE_PARALLEL_DECODE_STRATEGY.get() or "gather" if self.vae_parallel_decode_strategy not in strategies: raise ValueError(f"vae_parallel_decode_strategy must be one of {strategies}, " f"got {self.vae_parallel_decode_strategy!r}.") diff --git a/fastvideo/hooks/activation_trace.py b/fastvideo/hooks/activation_trace.py index c6a47a23b..b28d0608e 100644 --- a/fastvideo/hooks/activation_trace.py +++ b/fastvideo/hooks/activation_trace.py @@ -187,18 +187,18 @@ class ActivationTraceManager: def attach_activation_trace(model: nn.Module | None) -> ActivationTraceManager | None: """Attach activation-stat hooks to model. Returns None if trace is off.""" - if not envs.FASTVIDEO_TRACE_ACTIVATIONS or model is None: + if not envs.FASTVIDEO_TRACE_ACTIVATIONS.get() or model is None: return None - pattern_spec = envs.FASTVIDEO_TRACE_LAYERS + pattern_spec = envs.FASTVIDEO_TRACE_LAYERS.get() pattern = re.compile(pattern_spec) if pattern_spec else re.compile(".*") - stats = _resolve_stats(envs.FASTVIDEO_TRACE_STATS) + stats = _resolve_stats(envs.FASTVIDEO_TRACE_STATS.get()) if not stats: logger.warning("FASTVIDEO_TRACE_STATS yielded no valid stats; trace disabled.") return None - sink = JsonlSink(_resolve_output_path(envs.FASTVIDEO_TRACE_OUTPUT)) - step_filter = _parse_step_filter(envs.FASTVIDEO_TRACE_STEPS) + sink = JsonlSink(_resolve_output_path(envs.FASTVIDEO_TRACE_OUTPUT.get())) + step_filter = _parse_step_filter(envs.FASTVIDEO_TRACE_STEPS.get()) managers = [] for name, module in model.named_modules(): if not name or not pattern.search(name): diff --git a/fastvideo/logger.py b/fastvideo/logger.py index 84f9958b4..a930ae0e4 100644 --- a/fastvideo/logger.py +++ b/fastvideo/logger.py @@ -15,10 +15,10 @@ from typing import Any, cast import fastvideo.envs as envs -FASTVIDEO_CONFIGURE_LOGGING = envs.FASTVIDEO_CONFIGURE_LOGGING -FASTVIDEO_LOGGING_CONFIG_PATH = envs.FASTVIDEO_LOGGING_CONFIG_PATH -FASTVIDEO_LOGGING_LEVEL = envs.FASTVIDEO_LOGGING_LEVEL -FASTVIDEO_LOGGING_PREFIX = envs.FASTVIDEO_LOGGING_PREFIX +FASTVIDEO_CONFIGURE_LOGGING = envs.FASTVIDEO_CONFIGURE_LOGGING.get() +FASTVIDEO_LOGGING_CONFIG_PATH = envs.FASTVIDEO_LOGGING_CONFIG_PATH.get() +FASTVIDEO_LOGGING_LEVEL = envs.FASTVIDEO_LOGGING_LEVEL.get() +FASTVIDEO_LOGGING_PREFIX = envs.FASTVIDEO_LOGGING_PREFIX.get() RED = '\033[91m' GREEN = '\033[92m' diff --git a/fastvideo/models/dits/minimax_h3.py b/fastvideo/models/dits/minimax_h3.py index 370d11d47..1cd1d16d0 100644 --- a/fastvideo/models/dits/minimax_h3.py +++ b/fastvideo/models/dits/minimax_h3.py @@ -47,7 +47,7 @@ _MINIMAX_H3_FUSION_NAMES = frozenset({"modulate", "qknorm_rope", "swiglu"}) def _enabled_minimax_h3_fusions(value: str | None = None) -> frozenset[str]: """Parse the independently switchable inference fusion set.""" - raw = envs.FASTVIDEO_MINIMAX_H3_FUSIONS if value is None else value + raw = envs.FASTVIDEO_MINIMAX_H3_FUSIONS.get() if value is None else value normalized = raw.strip().lower() if normalized in {"", "0", "none"}: return frozenset() @@ -720,7 +720,7 @@ class MiniMaxH3Transformer3DModel(BaseDiT): fuse_modulate="modulate" in self.enabled_fusions, fuse_qknorm_rope="qknorm_rope" in self.enabled_fusions, fuse_swiglu="swiglu" in self.enabled_fusions, - fa4_packed_varlen=envs.FASTVIDEO_MINIMAX_H3_FA4_PACKED_VARLEN, + fa4_packed_varlen=envs.FASTVIDEO_MINIMAX_H3_FA4_PACKED_VARLEN.get(), ) for index in range(arch.num_layers) ]) self.norm_out = MiniMaxH3AdaLayerNormOut( diff --git a/fastvideo/models/loader/fsdp_load.py b/fastvideo/models/loader/fsdp_load.py index e076359ee..f7c31d221 100644 --- a/fastvideo/models/loader/fsdp_load.py +++ b/fastvideo/models/loader/fsdp_load.py @@ -19,6 +19,7 @@ from torch.distributed._tensor import distribute_tensor from torch.distributed.fsdp import (CPUOffloadPolicy, FSDPModule, MixedPrecisionPolicy, fully_shard) from torch.nn.modules.module import _IncompatibleKeys +import fastvideo.envs as envs from fastvideo.logger import init_logger from fastvideo.models.loader.lora_patch import DenseLoRAPatch from fastvideo.models.loader.utils import (get_param_names_mapping, hf_to_custom_state_dict) @@ -394,7 +395,7 @@ def _regional_compile_unsupported_reason( if os.environ.get("FASTVIDEO_H3_VSA_PROBE"): return ("FASTVIDEO_H3_VSA_PROBE records tensors and files from the VSA-H3 attention body, which " "regional fullgraph compile cannot capture; this model stays eager") - if os.environ.get("FASTVIDEO_VSA_SM100A", "0") != "1": + if not envs.FASTVIDEO_VSA_SM100A.get(): return ("VIDEO_SPARSE_ATTN_H3 regional compile requires the compile-safe sm_100a route " "(FASTVIDEO_VSA_SM100A=1); Triton/CuTe VSA stays eager") if vsa_tile_size != 64: diff --git a/fastvideo/pipelines/composed_pipeline_base.py b/fastvideo/pipelines/composed_pipeline_base.py index 183a34af4..d5c0d8230 100644 --- a/fastvideo/pipelines/composed_pipeline_base.py +++ b/fastvideo/pipelines/composed_pipeline_base.py @@ -162,7 +162,7 @@ class ComposedPipelineBase(ABC): # Torch profiler. Enabled and configured through env vars: # FASTVIDEO_TORCH_PROFILER_DIR=/path/to/save/trace - trace_dir = envs.FASTVIDEO_TORCH_PROFILER_DIR + trace_dir = envs.FASTVIDEO_TORCH_PROFILER_DIR.get() self.profiler_controller = get_or_create_profiler(trace_dir) self.local_rank = get_world_group().local_rank @@ -378,7 +378,7 @@ class ComposedPipelineBase(ABC): # The hook manager keeps a strong reference to every module it # wraps, so attaching here would materialize the DiT before the # first request and pin that instance past any release. - if envs.FASTVIDEO_TRACE_ACTIVATIONS: + if envs.FASTVIDEO_TRACE_ACTIVATIONS.get(): logger.warning("Activation trace is not attached to a deferred transformer; " "turn off lazy_module_load to trace it") trace_target = None diff --git a/fastvideo/pipelines/stages/base.py b/fastvideo/pipelines/stages/base.py index 0f990b256..838ce677a 100644 --- a/fastvideo/pipelines/stages/base.py +++ b/fastvideo/pipelines/stages/base.py @@ -178,7 +178,7 @@ class PipelineStage(ABC): stage_name: str, ) -> ForwardBatch: """Run forward, with the optional timing and logging wrapper.""" - if envs.FASTVIDEO_STAGE_LOGGING: + if envs.FASTVIDEO_STAGE_LOGGING.get(): logger.info("[%s] Starting execution", stage_name) torch.cuda.synchronize() start_time = time.perf_counter() diff --git a/fastvideo/pipelines/stages/denoising.py b/fastvideo/pipelines/stages/denoising.py index 79dbca32c..f36509eb3 100644 --- a/fastvideo/pipelines/stages/denoising.py +++ b/fastvideo/pipelines/stages/denoising.py @@ -283,7 +283,7 @@ class DenoisingStage(PipelineStage): # compute. See envs.py for semantics. delta_cached_model_id tracks # which underlying transformer produced the cache so we invalidate on # Wan2.2 expert switch. - _cfg_gate_fraction = envs.FASTVIDEO_CFG_GATE_STEP + _cfg_gate_fraction = envs.FASTVIDEO_CFG_GATE_STEP.get() if not 0.0 <= _cfg_gate_fraction <= 1.0: raise ValueError(f"FASTVIDEO_CFG_GATE_STEP must be in [0.0, 1.0], got {_cfg_gate_fraction!r}. " "Use 1.0 (default) to disable; lower values trade quality for speed.") diff --git a/fastvideo/platforms/cuda.py b/fastvideo/platforms/cuda.py index b6f8a9cb3..78c6cdc04 100644 --- a/fastvideo/platforms/cuda.py +++ b/fastvideo/platforms/cuda.py @@ -171,7 +171,7 @@ class CudaPlatformBase(Platform): # TODO(will): maybe come up with a more general interface for local attention # if distributed is False, we always try to use Flash attn - logger.info("Trying FASTVIDEO_ATTENTION_BACKEND=%s", envs.FASTVIDEO_ATTENTION_BACKEND) + logger.info("Trying FASTVIDEO_ATTENTION_BACKEND=%s", envs.FASTVIDEO_ATTENTION_BACKEND.get()) logger.info("Selected backend: %s", selected_backend) if selected_backend == AttentionBackendEnum.SAGE_ATTN: try: diff --git a/fastvideo/platforms/npu.py b/fastvideo/platforms/npu.py index 23e162dd1..c5014b1fa 100644 --- a/fastvideo/platforms/npu.py +++ b/fastvideo/platforms/npu.py @@ -66,8 +66,8 @@ class NPUPlatform(Platform): @classmethod def get_attn_backend_cls(cls, selected_backend: AttentionBackendEnum | None, head_size: int, dtype: torch.dtype) -> str: - logger.info("Trying FASTVIDEO_ATTENTION_BACKEND=%s", envs.FASTVIDEO_ATTENTION_BACKEND) - if envs.FASTVIDEO_ATTENTION_BACKEND != "TORCH_SDPA": + logger.info("Trying FASTVIDEO_ATTENTION_BACKEND=%s", envs.FASTVIDEO_ATTENTION_BACKEND.get()) + if envs.FASTVIDEO_ATTENTION_BACKEND.get() != "TORCH_SDPA": logger.info("Ascend NPU only supports the Torch SDPA backend.") else: logger.info("Using Torch SDPA backend.") diff --git a/fastvideo/platforms/rocm.py b/fastvideo/platforms/rocm.py index 461104621..4b8b8315d 100644 --- a/fastvideo/platforms/rocm.py +++ b/fastvideo/platforms/rocm.py @@ -63,7 +63,7 @@ class RocmPlatform(Platform): @classmethod def get_attn_backend_cls(cls, selected_backend: AttentionBackendEnum | None, head_size: int, dtype: torch.dtype) -> str: - logger.info("Trying FASTVIDEO_ATTENTION_BACKEND=%s", envs.FASTVIDEO_ATTENTION_BACKEND) + logger.info("Trying FASTVIDEO_ATTENTION_BACKEND=%s", envs.FASTVIDEO_ATTENTION_BACKEND.get()) if selected_backend == AttentionBackendEnum.TORCH_SDPA: logger.info("Using Torch SDPA backend.") diff --git a/fastvideo/profiler.py b/fastvideo/profiler.py index d75879d7b..bcfc9ab84 100644 --- a/fastvideo/profiler.py +++ b/fastvideo/profiler.py @@ -45,7 +45,7 @@ def nvtx_range(name: str): ``FASTVIDEO_NVTX_PROFILE=1`` enables the marker. The context manager stays a no-op without CUDA so call sites can remain shared with CPU tests. """ - enabled = envs.FASTVIDEO_NVTX_PROFILE and torch.cuda.is_available() + enabled = envs.FASTVIDEO_NVTX_PROFILE.get() and torch.cuda.is_available() if not enabled: yield return @@ -201,19 +201,19 @@ def get_or_create_profiler(trace_dir: str | None) -> TorchProfilerController: logger.info("Profiling enabled. Traces will be saved to: %s", trace_dir) logger.info( "Profiler config: record_shapes=%s, profile_memory=%s, with_stack=%s, with_flops=%s", - envs.FASTVIDEO_TORCH_PROFILER_RECORD_SHAPES, - envs.FASTVIDEO_TORCH_PROFILER_WITH_PROFILE_MEMORY, - envs.FASTVIDEO_TORCH_PROFILER_WITH_STACK, - envs.FASTVIDEO_TORCH_PROFILER_WITH_FLOPS, + envs.FASTVIDEO_TORCH_PROFILER_RECORD_SHAPES.get(), + envs.FASTVIDEO_TORCH_PROFILER_WITH_PROFILE_MEMORY.get(), + envs.FASTVIDEO_TORCH_PROFILER_WITH_STACK.get(), + envs.FASTVIDEO_TORCH_PROFILER_WITH_FLOPS.get(), ) - logger.info("FASTVIDEO_TORCH_PROFILE_REGIONS=%s", envs.FASTVIDEO_TORCH_PROFILE_REGIONS) + logger.info("FASTVIDEO_TORCH_PROFILE_REGIONS=%s", envs.FASTVIDEO_TORCH_PROFILE_REGIONS.get()) profiler = torch.profiler.profile( activities=_DEFAULT_ACTIVITIES, - record_shapes=envs.FASTVIDEO_TORCH_PROFILER_RECORD_SHAPES, - profile_memory=envs.FASTVIDEO_TORCH_PROFILER_WITH_PROFILE_MEMORY, - with_stack=envs.FASTVIDEO_TORCH_PROFILER_WITH_STACK, - with_flops=envs.FASTVIDEO_TORCH_PROFILER_WITH_FLOPS, + record_shapes=envs.FASTVIDEO_TORCH_PROFILER_RECORD_SHAPES.get(), + profile_memory=envs.FASTVIDEO_TORCH_PROFILER_WITH_PROFILE_MEMORY.get(), + with_stack=envs.FASTVIDEO_TORCH_PROFILER_WITH_STACK.get(), + with_flops=envs.FASTVIDEO_TORCH_PROFILER_WITH_FLOPS.get(), # No schedule: nothing in the codebase calls profiler.step(), so a # wait/warmup schedule never advances and the profiler records nothing. # Region toggling gates collection; the single trace exports at stop(). @@ -249,7 +249,7 @@ class TorchProfilerConfig: requested_regions = { token.strip() - for token in (getattr(envs, "FASTVIDEO_TORCH_PROFILE_REGIONS", "") or "").split(",") if token.strip() + for token in envs.FASTVIDEO_TORCH_PROFILE_REGIONS.get().split(",") if token.strip() } if not requested_regions: diff --git a/fastvideo/tests/contract/test_env_policy.py b/fastvideo/tests/contract/test_env_policy.py new file mode 100644 index 000000000..571e22228 --- /dev/null +++ b/fastvideo/tests/contract/test_env_policy.py @@ -0,0 +1,827 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Guard: FastVideo code follows the environment-variable policy in +docs/contributing/env_vars.md. + +The test parses every Python file under fastvideo/ (except fastvideo/third_party/ +and the registry fastvideo/envs.py) with ``ast`` and reports code that: + +- reads the environment directly for a name outside EXTERNAL_ALLOWLIST; +- writes the environment directly (os.environ, os.putenv, monkeypatch.setenv); +- uses the whole environment (os.environ.copy(), dict(os.environ), patch.dict); +- uses a registry field without calling one of its methods (``envs.X == "a"``, + ``getter = envs.X.get``); +- calls ``envs.X.get()`` outside a function, so the value is read at import. + +It also checks every entry in fastvideo/envs.py (FASTVIDEO_ prefix, category, +description, at least one reader) and checks that the table in +docs/contributing/env_vars.md matches the registry. + +Violations that existed when the policy was introduced are listed in +KNOWN_VIOLATIONS. The list only shrinks: a violation that is not listed fails, +and a listed violation that no longer exists also fails, so the entry gets +deleted. + +Run ``python fastvideo/tests/contract/test_env_policy.py`` to regenerate the +table in docs/contributing/env_vars.md after editing fastvideo/envs.py. + +Static analysis only: fastvideo/envs.py is loaded as a standalone file, so the +test imports neither fastvideo nor torch. +""" +import ast +import importlib.util +import re +from collections import Counter +from pathlib import Path +from types import ModuleType + +REPO_ROOT = Path(__file__).resolve().parents[3] +PACKAGE_ROOT = REPO_ROOT / "fastvideo" +REGISTRY_PATH = PACKAGE_ROOT / "envs.py" +DOC_PATH = REPO_ROOT / "docs" / "contributing" / "env_vars.md" +POLICY_DOC = "docs/contributing/env_vars.md" +EXCLUDED_DIRS = (PACKAGE_ROOT / "third_party", ) + +DOC_TABLE_BEGIN = "" +DOC_TABLE_END = "" + +# Variables that other tools own. Code may read them directly with a literal +# name. A trailing "*" allows every name with that prefix. +EXTERNAL_ALLOWLIST = { + "CUDA_*": "CUDA runtime and device selection.", + "NCCL_*": "NCCL communication library.", + "TORCH_*": "PyTorch runtime settings.", + "OMP_*": "OpenMP threading.", + "HF_HUB_*": "huggingface_hub settings.", + "RANK": "Set by torchrun and other launchers.", + "LOCAL_RANK": "Set by torchrun and other launchers.", + "WORLD_SIZE": "Set by torchrun and other launchers.", + "MASTER_ADDR": "Set by torchrun and other launchers.", + "MASTER_PORT": "Set by torchrun and other launchers.", + "HOME": "User home directory.", + "PATH": "Executable search path.", + "HF_TOKEN": "Token variable that huggingface_hub reads.", + "HUGGING_FACE_HUB_TOKEN": "Token variable that huggingface_hub reads.", +} + +# Methods that registry fields expose; ``get`` and ``is_set`` count as reads. +REGISTRY_METHODS = {"get", "set", "override", "is_set", "clear"} +REGISTRY_READ_METHODS = {"get", "is_set"} + +# Violations that existed when the policy was introduced, as +# ": " -> number of occurrences. Lower or delete an entry when +# its violations are fixed; never add one. Kinds are described in +# docs/contributing/env_vars.md. +KNOWN_VIOLATIONS: dict[str, int] = { + 'fastvideo/attention/backends/flash_attn.py: read FASTVIDEO_NVFP4_FA4': 1, + 'fastvideo/attention/backends/video_sparse_attn_h3_probe.py: read FASTVIDEO_H3_VSA_PROBE': 1, + 'fastvideo/attention/layer.py: read FASTVIDEO_DISABLE_ATTENTION_COMPILE': 2, + 'fastvideo/attention/selector.py: read ': 1, + 'fastvideo/attention/utils/flash_attn_default.py: import-time-read FASTVIDEO_FA4': 1, + 'fastvideo/benchmarks/eval_metalfx_rife.py: write FASTVIDEO_MLX_COMPILE': 1, + 'fastvideo/benchmarks/mlx_fastwan_bench.py: read FASTVIDEO_MLX_COMPILE': 1, + 'fastvideo/benchmarks/mlx_fastwan_bench.py: read FASTVIDEO_MLX_FAST_NORM': 1, + 'fastvideo/benchmarks/mlx_fastwan_bench.py: write FASTVIDEO_MLX_COMPILE': 1, + 'fastvideo/distributed/device_communicators/cpu_communicator.py: read VLLM_DIST_IDENT': 1, + 'fastvideo/entrypoints/cli/utils.py: whole-environ': 1, + 'fastvideo/entrypoints/openai/api_server.py: write FASTVIDEO_STAGE_LOGGING': 1, + 'fastvideo/entrypoints/streaming/prompt/providers/cerebras.py: read ': 1, + 'fastvideo/entrypoints/streaming/prompt/providers/groq.py: read ': 1, + 'fastvideo/entrypoints/streaming/worker.py: write CUDA_VISIBLE_DEVICES': 1, + 'fastvideo/entrypoints/video_generator.py: read FASTVIDEO_FFMPEG_BIN': 1, + 'fastvideo/entrypoints/video_generator.py: read FASTVIDEO_NVENC_BF': 1, + 'fastvideo/entrypoints/video_generator.py: read FASTVIDEO_NVENC_PRESET': 1, + 'fastvideo/entrypoints/video_generator.py: read FASTVIDEO_NVENC_QP': 1, + 'fastvideo/entrypoints/video_generator.py: read FASTVIDEO_NVENC_RC': 1, + 'fastvideo/entrypoints/video_generator.py: read FASTVIDEO_NVENC_TUNE': 1, + 'fastvideo/entrypoints/video_generator.py: read FASTVIDEO_OUTPUT_PIX_FMT': 1, + 'fastvideo/entrypoints/video_generator.py: read FASTVIDEO_VIDEO_CODEC': 1, + 'fastvideo/entrypoints/video_generator.py: read FASTVIDEO_X264_PRESET': 1, + 'fastvideo/entrypoints/video_generator.py: write CUTE_DSL_ENABLE_TVM_FFI': 1, + 'fastvideo/entrypoints/video_generator.py: write FASTVIDEO_NVFP4_FA4': 1, + 'fastvideo/envs.py: prefix CMAKE_BUILD_TYPE': 1, + 'fastvideo/envs.py: prefix CUDA_VISIBLE_DEVICES': 1, + 'fastvideo/envs.py: prefix HCCL_SO_PATH': 1, + 'fastvideo/envs.py: prefix LD_LIBRARY_PATH': 1, + 'fastvideo/envs.py: prefix LOCAL_RANK': 1, + 'fastvideo/envs.py: prefix MAX_JOBS': 1, + 'fastvideo/envs.py: prefix NVCC_THREADS': 1, + 'fastvideo/envs.py: prefix VERBOSE': 1, + 'fastvideo/envs.py: unread CMAKE_BUILD_TYPE': 1, + 'fastvideo/envs.py: unread CUDA_VISIBLE_DEVICES': 1, + 'fastvideo/envs.py: unread FASTVIDEO_ENGINE_ITERATION_TIMEOUT_S': 1, + 'fastvideo/envs.py: unread FASTVIDEO_RINGBUFFER_WARNING_INTERVAL': 1, + 'fastvideo/envs.py: unread FASTVIDEO_SERVER_DEV_MODE': 1, + 'fastvideo/envs.py: unread FASTVIDEO_TARGET_DEVICE': 1, + 'fastvideo/envs.py: unread FASTVIDEO_TEST_DYNAMO_FULLGRAPH_CAPTURE': 1, + 'fastvideo/envs.py: unread FASTVIDEO_TRACE_FUNCTION': 1, + 'fastvideo/envs.py: unread FASTVIDEO_USE_PRECOMPILED': 1, + 'fastvideo/envs.py: unread LD_LIBRARY_PATH': 1, + 'fastvideo/envs.py: unread MAX_JOBS': 1, + 'fastvideo/envs.py: unread NVCC_THREADS': 1, + 'fastvideo/envs.py: unread VERBOSE': 1, + 'fastvideo/eval/__init__.py: write TORCH_HOME': 1, + 'fastvideo/eval/datasets/physics_iq.py: read FASTVIDEO_PHYSICS_IQ_BUCKET_URL': 1, + 'fastvideo/eval/datasets/vbench.py: read VBENCH_FULL_INFO_JSON': 1, + 'fastvideo/eval/metrics/audio/frechet_distance/metric.py: read ': 1, + 'fastvideo/eval/metrics/common/fvd/metric.py: read ': 1, + 'fastvideo/eval/metrics/judge/third_person_separation/metric.py: read ': 1, + 'fastvideo/eval/metrics/vbench/scene/metric.py: write VIDEO_MAX_PIXELS': 1, + 'fastvideo/eval/models.py: read FASTVIDEO_EVAL_CACHE': 1, + 'fastvideo/logger.py: import-time-read FASTVIDEO_CONFIGURE_LOGGING': 1, + 'fastvideo/logger.py: import-time-read FASTVIDEO_LOGGING_CONFIG_PATH': 1, + 'fastvideo/logger.py: import-time-read FASTVIDEO_LOGGING_LEVEL': 1, + 'fastvideo/logger.py: import-time-read FASTVIDEO_LOGGING_PREFIX': 1, + 'fastvideo/mlx_runtime/fastwan.py: read FASTVIDEO_MLX_COMPILE': 1, + 'fastvideo/mlx_runtime/fastwan.py: read FASTVIDEO_MLX_DQ_GEMM': 1, + 'fastvideo/mlx_runtime/fastwan.py: read FASTVIDEO_MLX_FAST_NORM': 1, + 'fastvideo/mlx_runtime/fastwan.py: read FASTVIDEO_MLX_WINDOW': 1, + 'fastvideo/mlx_runtime/fastwan.py: read FASTVIDEO_MLX_WINDOW_SINK': 1, + 'fastvideo/mlx_runtime/memory.py: write ': 1, + 'fastvideo/mlx_runtime/wan22.py: read FASTVIDEO_MLX_COMPILE': 1, + 'fastvideo/models/encoders/gemma.py: read LTX2_FASTVIDEO_GEMMA_LOG': 2, + 'fastvideo/models/encoders/reason1.py: read FASTVIDEO_REASON1_WEIGHTS_PATH': 1, + 'fastvideo/models/loader/benchmarks/benchmark_weight_loading.py: write MASTER_ADDR': 1, + 'fastvideo/models/loader/benchmarks/benchmark_weight_loading.py: write MASTER_PORT': 1, + 'fastvideo/models/loader/benchmarks/benchmark_weight_loading.py: write RANK': 1, + 'fastvideo/models/loader/benchmarks/benchmark_weight_loading.py: write WORLD_SIZE': 1, + 'fastvideo/models/loader/fsdp_load.py: read FASTVIDEO_FSDP2_AUTOWRAP': 1, + 'fastvideo/models/loader/fsdp_load.py: read FASTVIDEO_FSDP2_MIN_PARAMS': 1, + 'fastvideo/models/loader/fsdp_load.py: read FASTVIDEO_H3_VSA_PROBE': 1, + 'fastvideo/models/vaes/ltx2vae.py: read ': 1, + 'fastvideo/performance/hf_store.py: read HF_REPO_ID': 1, + 'fastvideo/performance/hf_store.py: read PERFORMANCE_TRACKING_SYNC_REUSE_TTL_SECONDS': 1, + 'fastvideo/performance_dashboard/api.py: read PERFORMANCE_TRACKING_ROOT': 1, + 'fastvideo/pipelines/basic/ltx2/stages/ltx2_audio_decoding.py: read LTX2_DISABLE_AUDIO_AUTOCAST': 1, + 'fastvideo/pipelines/basic/ltx2/stages/ltx2_denoising.py: read FASTVIDEO_NVTX_PROFILE': 1, + 'fastvideo/pipelines/basic/ltx2/stages/ltx2_denoising.py: read LTX2_USE_DISTILLED_SIGMAS': 1, + 'fastvideo/pipelines/basic/magi_human/magi_human_pipeline.py: write HF_TOKEN': 1, + 'fastvideo/pipelines/preprocess/preprocess_kandinsky5_overfit.py: read KANDINSKY5_OVERFIT_DATA_DIR': 1, + 'fastvideo/pipelines/preprocess/preprocess_kandinsky5_overfit.py: read KANDINSKY5_OVERFIT_OUTPUT_DIR': 1, + 'fastvideo/pipelines/preprocess/preprocess_kandinsky5_overfit.py: write MASTER_ADDR': 1, + 'fastvideo/pipelines/preprocess/preprocess_kandinsky5_overfit.py: write MASTER_PORT': 1, + 'fastvideo/pipelines/preprocess/preprocess_ltx2_overfit.py: read LTX2_OVERFIT_CAPTION_JSON': 1, + 'fastvideo/pipelines/preprocess/preprocess_ltx2_overfit.py: read LTX2_OVERFIT_DATA_DIR': 1, + 'fastvideo/pipelines/preprocess/preprocess_ltx2_overfit.py: read LTX2_OVERFIT_MODEL': 1, + 'fastvideo/pipelines/preprocess/preprocess_ltx2_overfit.py: read LTX2_OVERFIT_NUM_COPIES': 1, + 'fastvideo/pipelines/preprocess/preprocess_ltx2_overfit.py: read LTX2_OVERFIT_OUTPUT_DIR': 1, + 'fastvideo/pipelines/preprocess/preprocess_ltx2_overfit.py: read LTX2_OVERFIT_VIDEO_SUBDIR': 1, + 'fastvideo/pipelines/preprocess/preprocess_ltx2_overfit.py: write LOCAL_RANK': 1, + 'fastvideo/pipelines/preprocess/preprocess_ltx2_overfit.py: write MASTER_ADDR': 1, + 'fastvideo/pipelines/preprocess/preprocess_ltx2_overfit.py: write MASTER_PORT': 1, + 'fastvideo/pipelines/preprocess/preprocess_ltx2_overfit.py: write RANK': 1, + 'fastvideo/pipelines/preprocess/preprocess_ltx2_overfit.py: write WORLD_SIZE': 1, + 'fastvideo/pipelines/preprocess/preprocess_minimax_h3_overfit.py: write LOCAL_RANK': 1, + 'fastvideo/pipelines/preprocess/preprocess_minimax_h3_overfit.py: write MASTER_ADDR': 1, + 'fastvideo/pipelines/preprocess/preprocess_minimax_h3_overfit.py: write MASTER_PORT': 1, + 'fastvideo/pipelines/preprocess/preprocess_minimax_h3_overfit.py: write RANK': 1, + 'fastvideo/pipelines/preprocess/preprocess_minimax_h3_overfit.py: write WORLD_SIZE': 1, + 'fastvideo/pipelines/stages/denoising.py: read FASTVIDEO_FLUX2_DISABLE_BF16_REDUCED_PRECISION_REDUCTION': 1, + 'fastvideo/pipelines/stages/latent_preparation.py: read FASTVIDEO_COSMOS25_LOG_KNOBS': 1, + 'fastvideo/tests/api/test_attention_selector_resolution.py: write FASTVIDEO_ATTENTION_BACKEND': 17, + 'fastvideo/tests/attention/test_flash_attn_no_pad_resolver.py: write FASTVIDEO_FA4': 3, + 'fastvideo/tests/attention/test_flash_attn_nvfp4_opt_out.py: write FASTVIDEO_NVFP4_FA4': 4, + 'fastvideo/tests/attention/test_vsa_h3_backward.py: write FASTVIDEO_KERNEL_VSA_FORCE_TRITON': 1, + 'fastvideo/tests/attention/test_vsa_h3_backward.py: write FASTVIDEO_VSA_CUTEDSL': 2, + 'fastvideo/tests/attention/test_vsa_h3_backward.py: write FASTVIDEO_VSA_TRITON': 2, + 'fastvideo/tests/attention/test_vsa_h3_sm100a_route.py: write ': 22, + 'fastvideo/tests/contract/test_profiler_regions.py: whole-environ': 2, + 'fastvideo/tests/contract/test_profiler_regions.py: write FASTVIDEO_NVTX_PROFILE': 4, + 'fastvideo/tests/contract/test_ssim_ci_runner.py: write CUDA_VISIBLE_DEVICES': 1, + 'fastvideo/tests/contract/test_tensor_golden_isolation.py: read FASTVIDEO_ATTENTION_BACKEND': 2, + 'fastvideo/tests/contract/test_tensor_golden_isolation.py: write FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/contract/test_wan_validation_order.py: whole-environ': 1, + 'fastvideo/tests/distributed/test_sp_hunyuanvideo.py: whole-environ': 1, + 'fastvideo/tests/distributed/test_sp_lingbot_video.py: whole-environ': 1, + 'fastvideo/tests/distributed/test_sp_ltx2.py: whole-environ': 1, + 'fastvideo/tests/distributed/test_sp_wan.py: whole-environ': 1, + 'fastvideo/tests/distributed/test_ulysses_a2a_parity.py: whole-environ': 1, + 'fastvideo/tests/distributed/test_ulysses_fault_injection.py: read FASTVIDEO_ULYSSES_FAULT_RANK': 1, + 'fastvideo/tests/distributed/test_ulysses_fault_injection.py: read FASTVIDEO_ULYSSES_FAULT_STAGE': 1, + 'fastvideo/tests/distributed/test_ulysses_fault_injection.py: whole-environ': 1, + 'fastvideo/tests/distributed/test_ulysses_fault_injection.py: write FASTVIDEO_ULYSSES_A2A': 1, + 'fastvideo/tests/encoders/test_clip_encoder.py: write MASTER_ADDR': 1, + 'fastvideo/tests/encoders/test_clip_encoder.py: write MASTER_PORT': 1, + 'fastvideo/tests/encoders/test_hyt5_encoder.py: write MASTER_ADDR': 1, + 'fastvideo/tests/encoders/test_hyt5_encoder.py: write MASTER_PORT': 1, + 'fastvideo/tests/encoders/test_llama_encoder.py: write MASTER_ADDR': 1, + 'fastvideo/tests/encoders/test_llama_encoder.py: write MASTER_PORT': 1, + 'fastvideo/tests/encoders/test_minimax_h3_qwen3_vl_checkpoint_fp8.py: write MASTER_ADDR': 1, + 'fastvideo/tests/encoders/test_minimax_h3_qwen3_vl_checkpoint_fp8.py: write MASTER_PORT': 1, + 'fastvideo/tests/encoders/test_minimax_h3_qwen3_vl_checkpoint_nvfp4.py: write MASTER_ADDR': 1, + 'fastvideo/tests/encoders/test_minimax_h3_qwen3_vl_checkpoint_nvfp4.py: write MASTER_PORT': 1, + 'fastvideo/tests/encoders/test_minimax_h3_qwen3_vl_truncation.py: write MASTER_ADDR': 1, + 'fastvideo/tests/encoders/test_minimax_h3_qwen3_vl_truncation.py: write MASTER_PORT': 1, + 'fastvideo/tests/encoders/test_qwen2_5_encoder.py: write MASTER_ADDR': 1, + 'fastvideo/tests/encoders/test_qwen2_5_encoder.py: write MASTER_PORT': 1, + 'fastvideo/tests/encoders/test_siglip_encoder.py: write MASTER_ADDR': 1, + 'fastvideo/tests/encoders/test_siglip_encoder.py: write MASTER_PORT': 1, + 'fastvideo/tests/encoders/test_t5_encoder.py: write MASTER_ADDR': 1, + 'fastvideo/tests/encoders/test_t5_encoder.py: write MASTER_PORT': 1, + 'fastvideo/tests/entrypoints/streaming/test_prompt_providers.py: write CEREBRAS_API_KEY': 3, + 'fastvideo/tests/entrypoints/streaming/test_prompt_providers.py: write GROQ_API_KEY': 2, + 'fastvideo/tests/entrypoints/test_openai_api_integration.py: whole-environ': 1, + 'fastvideo/tests/entrypoints/test_openai_video_client.py: whole-environ': 1, + 'fastvideo/tests/entrypoints/test_video_generator.py: write FASTVIDEO_FFMPEG_BIN': 1, + 'fastvideo/tests/entrypoints/test_video_generator.py: write FASTVIDEO_OUTPUT_PIX_FMT': 1, + 'fastvideo/tests/entrypoints/test_video_generator.py: write FASTVIDEO_VIDEO_CODEC': 1, + 'fastvideo/tests/golden_gate/_harness.py: read ': 1, + 'fastvideo/tests/golden_gate/_harness.py: read FASTVIDEO_FA4': 2, + 'fastvideo/tests/golden_gate/_harness.py: read FASTVIDEO_GOLDEN_GATE_DIR': 1, + 'fastvideo/tests/golden_gate/_harness.py: read FASTVIDEO_SSIM_REFERENCE_HF_REPO': 1, + 'fastvideo/tests/golden_gate/_harness.py: write CUBLAS_WORKSPACE_CONFIG': 1, + 'fastvideo/tests/golden_gate/_harness.py: write FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/golden_gate/_harness.py: write LOCAL_RANK': 1, + 'fastvideo/tests/golden_gate/_harness.py: write MASTER_ADDR': 1, + 'fastvideo/tests/golden_gate/_harness.py: write MASTER_PORT': 1, + 'fastvideo/tests/golden_gate/_harness.py: write RANK': 1, + 'fastvideo/tests/golden_gate/_harness.py: write WORLD_SIZE': 1, + 'fastvideo/tests/golden_gate/_tensor_golden.py: read FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/golden_gate/_tensor_golden.py: write FASTVIDEO_ATTENTION_BACKEND': 3, + 'fastvideo/tests/golden_gate/test_kandinsky5.py: read FASTVIDEO_FA4': 1, + 'fastvideo/tests/golden_gate/test_kandinsky5.py: write FASTVIDEO_FA4': 1, + 'fastvideo/tests/golden_gate/test_minimax_h3_t2v.py: read FASTVIDEO_ATTENTION_BACKEND': 2, + 'fastvideo/tests/golden_gate/test_minimax_h3_t2v.py: read FASTVIDEO_FA4': 1, + 'fastvideo/tests/golden_gate/test_minimax_h3_t2v.py: read FASTVIDEO_SSIM_REFERENCE_HF_REPO': 1, + 'fastvideo/tests/golden_gate/test_minimax_h3_t2v.py: read MINIMAX_H3_GATE_GOLDEN_DIR': 1, + 'fastvideo/tests/golden_gate/test_minimax_h3_t2v.py: read MINIMAX_H3_GATE_LAYER': 1, + 'fastvideo/tests/golden_gate/test_minimax_h3_t2v.py: read MINIMAX_H3_MODEL_ROOT': 1, + 'fastvideo/tests/golden_gate/test_minimax_h3_t2v.py: write CUBLAS_WORKSPACE_CONFIG': 1, + 'fastvideo/tests/golden_gate/test_minimax_h3_t2v.py: write FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/golden_gate/test_minimax_h3_t2v.py: write LOCAL_RANK': 1, + 'fastvideo/tests/golden_gate/test_minimax_h3_t2v.py: write MASTER_ADDR': 1, + 'fastvideo/tests/golden_gate/test_minimax_h3_t2v.py: write MASTER_PORT': 1, + 'fastvideo/tests/golden_gate/test_minimax_h3_t2v.py: write RANK': 1, + 'fastvideo/tests/golden_gate/test_minimax_h3_t2v.py: write WORLD_SIZE': 1, + 'fastvideo/tests/golden_gate/test_wan_denoising.py: write FASTVIDEO_CFG_GATE_STEP': 1, + 'fastvideo/tests/hooks/test_activation_trace.py: write FASTVIDEO_TRACE_ACTIVATIONS': 6, + 'fastvideo/tests/hooks/test_activation_trace.py: write FASTVIDEO_TRACE_LAYERS': 5, + 'fastvideo/tests/hooks/test_activation_trace.py: write FASTVIDEO_TRACE_OUTPUT': 5, + 'fastvideo/tests/hooks/test_activation_trace.py: write FASTVIDEO_TRACE_STATS': 1, + 'fastvideo/tests/hooks/test_activation_trace.py: write FASTVIDEO_TRACE_STEPS': 1, + 'fastvideo/tests/inference/bsa/test_bsa_inference.py: write FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/inference/lora/test_lora_inference_similarity.py: write FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/inference/lora/test_lora_inference_similarity.py: write MASTER_ADDR': 1, + 'fastvideo/tests/inference/lora/test_lora_inference_similarity.py: write MASTER_PORT': 1, + 'fastvideo/tests/inference/test_basic_fasth3_profile.py: write ': 2, + 'fastvideo/tests/inference/test_inference_regional_compile.py: write FASTVIDEO_DISABLE_ATTENTION_COMPILE': 9, + 'fastvideo/tests/inference/test_inference_regional_compile.py: write FASTVIDEO_H3_VSA_PROBE': 4, + 'fastvideo/tests/inference/test_inference_regional_compile.py: write FASTVIDEO_VSA_SM100A': 3, + 'fastvideo/tests/inference/vmoba/test_vmoba_inference.py: write FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/layers/test_rmsnorm_forward_dispatch.py: whole-environ': 1, + 'fastvideo/tests/mlx/test_memory_limits.py: read PYTORCH_MPS_HIGH_WATERMARK_RATIO': 1, + 'fastvideo/tests/mlx/test_memory_limits.py: read PYTORCH_MPS_LOW_WATERMARK_RATIO': 1, + 'fastvideo/tests/mlx/test_memory_limits.py: write PYTORCH_MPS_HIGH_WATERMARK_RATIO': 1, + 'fastvideo/tests/mlx/test_memory_limits.py: write PYTORCH_MPS_LOW_WATERMARK_RATIO': 1, + 'fastvideo/tests/mlx/test_mlx_affine_dq_gemm.py: read FASTVIDEO_MLX_DQ_GEMM': 1, + 'fastvideo/tests/mlx/test_mlx_affine_dq_gemm.py: write FASTVIDEO_MLX_DQ_GEMM': 11, + 'fastvideo/tests/mlx/test_mlx_minimax_h3_vsa_regressions.py: write MASTER_ADDR': 1, + 'fastvideo/tests/mlx/test_mlx_minimax_h3_vsa_regressions.py: write MASTER_PORT': 1, + 'fastvideo/tests/mlx/test_mlx_taeh3.py: read TAEH3_REFERENCE_DIR': 1, + 'fastvideo/tests/mlx/test_mlx_wan22_prompt_cache_fingerprint.py: write HOME': 1, + 'fastvideo/tests/mlx/test_mlx_wan22_real_weights_parity.py: read FASTVIDEO_WAN22_5B_ALLOW_LOW_MEMORY': 1, + 'fastvideo/tests/mlx/test_mlx_wan22_real_weights_parity.py: read FASTVIDEO_WAN22_5B_ROOT': 1, + 'fastvideo/tests/mlx/test_mlx_wan22_real_weights_parity.py: write MASTER_ADDR': 1, + 'fastvideo/tests/mlx/test_mlx_wan22_real_weights_parity.py: write MASTER_PORT': 1, + 'fastvideo/tests/mlx/tiny_h3.py: write FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/mlx/tiny_h3.py: write MASTER_ADDR': 1, + 'fastvideo/tests/mlx/tiny_h3.py: write MASTER_PORT': 1, + 'fastvideo/tests/mlx/tiny_wan.py: write FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/mlx/tiny_wan.py: write MASTER_ADDR': 1, + 'fastvideo/tests/mlx/tiny_wan.py: write MASTER_PORT': 1, + 'fastvideo/tests/modal/kernel_build_cache.py: read ': 1, + 'fastvideo/tests/modal/kernel_build_cache.py: read CFLAGS': 1, + 'fastvideo/tests/modal/kernel_build_cache.py: read CMAKE_ARGS': 1, + 'fastvideo/tests/modal/kernel_build_cache.py: read CXXFLAGS': 1, + 'fastvideo/tests/modal/kernel_build_cache.py: read FASTVIDEO_KERNEL_CACHE_ROOT': 1, + 'fastvideo/tests/modal/kernel_build_cache.py: read FASTVIDEO_KERNEL_PREBUILT_INFO': 1, + 'fastvideo/tests/modal/kernel_build_cache.py: read GPU_BACKEND': 1, + 'fastvideo/tests/modal/kernel_build_cache.py: read LDFLAGS': 1, + 'fastvideo/tests/modal/kernel_build_cache.py: whole-environ': 1, + 'fastvideo/tests/modal/kernel_cache_smoke.py: read BUILDKITE_COMMIT': 1, + 'fastvideo/tests/modal/kernel_cache_smoke.py: read BUILDKITE_REPO': 1, + 'fastvideo/tests/modal/kernel_cache_smoke.py: read FASTVIDEO_MODAL_IMAGE': 1, + 'fastvideo/tests/modal/kernel_cache_smoke.py: read IMAGE_VERSION': 1, + 'fastvideo/tests/modal/kernel_cache_smoke.py: read UV_TORCH_BACKEND': 1, + 'fastvideo/tests/modal/launch_l40s_job.py: read ': 2, + 'fastvideo/tests/modal/launch_l40s_job.py: read BUILDKITE_COMMIT': 1, + 'fastvideo/tests/modal/launch_l40s_job.py: read BUILDKITE_PULL_REQUEST': 1, + 'fastvideo/tests/modal/launch_l40s_job.py: read BUILDKITE_REPO': 1, + 'fastvideo/tests/modal/launch_l40s_job.py: read FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/modal/launch_l40s_job.py: read FASTVIDEO_FA4': 1, + 'fastvideo/tests/modal/launch_l40s_job.py: read FASTVIDEO_MODAL_IMAGE': 1, + 'fastvideo/tests/modal/launch_l40s_job.py: read FASTVIDEO_MODAL_VOLUME': 1, + 'fastvideo/tests/modal/launch_l40s_job.py: read FASTVIDEO_PERFORMANCE_PROFILE_VERSION': 2, + 'fastvideo/tests/modal/launch_l40s_job.py: read IMAGE_VERSION': 1, + 'fastvideo/tests/modal/launch_l40s_job.py: read UV_TORCH_BACKEND': 1, + 'fastvideo/tests/modal/launch_l40s_job.py: whole-environ': 1, + 'fastvideo/tests/modal/modal_image_utils.py: read UV_TORCH_BACKEND': 1, + 'fastvideo/tests/modal/pr_test.py: read ': 2, + 'fastvideo/tests/modal/pr_test.py: read BUILDKITE_BRANCH': 1, + 'fastvideo/tests/modal/pr_test.py: read BUILDKITE_BUILD_ID': 1, + 'fastvideo/tests/modal/pr_test.py: read BUILDKITE_BUILD_URL': 1, + 'fastvideo/tests/modal/pr_test.py: read BUILDKITE_COMMIT': 2, + 'fastvideo/tests/modal/pr_test.py: read BUILDKITE_JOB_ID': 1, + 'fastvideo/tests/modal/pr_test.py: read BUILDKITE_PULL_REQUEST': 2, + 'fastvideo/tests/modal/pr_test.py: read BUILDKITE_REPO': 2, + 'fastvideo/tests/modal/pr_test.py: read BUILDKITE_SOURCE': 1, + 'fastvideo/tests/modal/pr_test.py: read FASTVIDEO_FA4': 1, + 'fastvideo/tests/modal/pr_test.py: read HF_API_KEY': 1, + 'fastvideo/tests/modal/pr_test.py: read IMAGE_VERSION': 1, + 'fastvideo/tests/modal/pr_test.py: read TEST_SCOPE': 1, + 'fastvideo/tests/modal/pr_test.py: read UV_TORCH_BACKEND': 1, + 'fastvideo/tests/modal/pr_test.py: read WANDB_API_KEY': 1, + 'fastvideo/tests/modal/ssim_test.py: read ': 1, + 'fastvideo/tests/modal/ssim_test.py: read BUILDKITE_COMMIT': 1, + 'fastvideo/tests/modal/ssim_test.py: read BUILDKITE_PULL_REQUEST': 1, + 'fastvideo/tests/modal/ssim_test.py: read BUILDKITE_REPO': 1, + 'fastvideo/tests/modal/ssim_test.py: read FASTVIDEO_FA4': 1, + 'fastvideo/tests/modal/ssim_test.py: read IMAGE_VERSION': 1, + 'fastvideo/tests/modal/ssim_test.py: read UV_TORCH_BACKEND': 1, + 'fastvideo/tests/modal/ssim_test.py: whole-environ': 2, + 'fastvideo/tests/modal/test_kernel_build_cache.py: write ': 2, + 'fastvideo/tests/modal/test_kernel_build_cache.py: write TORCH_CUDA_ARCH_LIST': 2, + 'fastvideo/tests/modal/test_pr_test.py: write BUILDKITE_COMMIT': 2, + 'fastvideo/tests/modal/test_pr_test.py: write BUILDKITE_PULL_REQUEST': 2, + 'fastvideo/tests/modal/test_pr_test.py: write BUILDKITE_REPO': 2, + 'fastvideo/tests/nightly/test_e2e_dmd_t2v_crush_smol.py: write WANDB_MODE': 1, + 'fastvideo/tests/nightly/test_e2e_i2v_overfit_single_sample.py: write WANDB_MODE': 1, + 'fastvideo/tests/nightly/test_e2e_kandinsky5_dmd_t2v_overfit.py: read ': 1, + 'fastvideo/tests/nightly/test_e2e_kandinsky5_dmd_t2v_overfit.py: read KANDINSKY5_E2E_NUM_GPUS': 1, + 'fastvideo/tests/nightly/test_e2e_kandinsky5_dmd_t2v_overfit.py: whole-environ': 3, + 'fastvideo/tests/nightly/test_e2e_kandinsky5_dmd_t2v_overfit.py: write WANDB_MODE': 1, + 'fastvideo/tests/nightly/test_e2e_ltx2_overfit_new_stack.py: read FASTVIDEO_NIGHTLY': 1, + 'fastvideo/tests/nightly/test_e2e_ltx2_overfit_new_stack.py: whole-environ': 2, + 'fastvideo/tests/nightly/test_e2e_ltx2_overfit_new_stack.py: write FASTVIDEO_NIGHTLY': 1, + 'fastvideo/tests/nightly/test_e2e_overfit_single_sample.py: write WANDB_MODE': 1, + 'fastvideo/tests/performance/compare_baseline.py: read BUILDKITE_BRANCH': 2, + 'fastvideo/tests/performance/compare_baseline.py: read BUILDKITE_BUILD_ID': 1, + 'fastvideo/tests/performance/compare_baseline.py: read BUILDKITE_BUILD_URL': 1, + 'fastvideo/tests/performance/compare_baseline.py: read BUILDKITE_COMMIT': 3, + 'fastvideo/tests/performance/compare_baseline.py: read BUILDKITE_JOB_ID': 1, + 'fastvideo/tests/performance/compare_baseline.py: read BUILDKITE_PULL_REQUEST': 2, + 'fastvideo/tests/performance/compare_baseline.py: read GITHUB_STEP_SUMMARY': 1, + 'fastvideo/tests/performance/compare_baseline.py: read PERFORMANCE_TRACKING_ROOT': 1, + 'fastvideo/tests/performance/compare_baseline.py: read PERF_PYTEST_RC': 4, + 'fastvideo/tests/performance/compare_baseline.py: read PERF_REPORTS_DIR': 1, + 'fastvideo/tests/performance/compare_baseline.py: read PERF_RUN_SOURCE': 1, + 'fastvideo/tests/performance/compare_baseline.py: read PERF_UPLOAD_POLICY': 1, + 'fastvideo/tests/performance/compare_baseline.py: read TEST_SCOPE': 2, + 'fastvideo/tests/performance/dashboard.py: read BUILDKITE_COMMIT': 1, + 'fastvideo/tests/performance/dashboard.py: read DASHBOARD_DAYS': 1, + 'fastvideo/tests/performance/dashboard.py: read PERFORMANCE_TRACKING_ROOT': 1, + 'fastvideo/tests/performance/dashboard.py: read PERF_REPORTS_DIR': 1, + 'fastvideo/tests/performance/identity.py: read FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/performance/identity.py: whole-environ': 1, + 'fastvideo/tests/performance/seed_baseline.py: read PERFORMANCE_RESEED_STAGING_ROOT': 1, + 'fastvideo/tests/performance/seed_baseline.py: read PERFORMANCE_TRACKING_ROOT': 1, + 'fastvideo/tests/performance/seed_baseline.py: read USER': 1, + 'fastvideo/tests/performance/test_compare_baseline_policy.py: write BUILDKITE_BRANCH': 3, + 'fastvideo/tests/performance/test_compare_baseline_policy.py: write BUILDKITE_BUILD_ID': 3, + 'fastvideo/tests/performance/test_compare_baseline_policy.py: write BUILDKITE_BUILD_URL': 2, + 'fastvideo/tests/performance/test_compare_baseline_policy.py: write BUILDKITE_JOB_ID': 3, + 'fastvideo/tests/performance/test_compare_baseline_policy.py: write BUILDKITE_PULL_REQUEST': 3, + 'fastvideo/tests/performance/test_compare_baseline_policy.py: write PERF_PYTEST_RC': 8, + 'fastvideo/tests/performance/test_compare_baseline_policy.py: write PERF_RUN_SOURCE': 23, + 'fastvideo/tests/performance/test_compare_baseline_policy.py: write TEST_SCOPE': 3, + 'fastvideo/tests/performance/test_dashboard_service.py: write ': 1, + 'fastvideo/tests/performance/test_dashboard_service.py: write HF_TOKEN': 1, + 'fastvideo/tests/performance/test_inference_performance.py: read BUILDKITE_BRANCH': 2, + 'fastvideo/tests/performance/test_inference_performance.py: read BUILDKITE_BUILD_ID': 1, + 'fastvideo/tests/performance/test_inference_performance.py: read BUILDKITE_BUILD_URL': 1, + 'fastvideo/tests/performance/test_inference_performance.py: read BUILDKITE_COMMIT': 2, + 'fastvideo/tests/performance/test_inference_performance.py: read BUILDKITE_JOB_ID': 1, + 'fastvideo/tests/performance/test_inference_performance.py: read BUILDKITE_PULL_REQUEST': 2, + 'fastvideo/tests/performance/test_inference_performance.py: read FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/performance/test_inference_performance.py: read FASTVIDEO_FA4': 1, + 'fastvideo/tests/performance/test_inference_performance.py: read FASTVIDEO_PERFORMANCE_PROFILE_VERSION': 1, + 'fastvideo/tests/performance/test_inference_performance.py: read FASTVIDEO_STAGE_LOGGING': 1, + 'fastvideo/tests/performance/test_inference_performance.py: read IMAGE_VERSION': 1, + 'fastvideo/tests/performance/test_inference_performance.py: read PERF_RUN_SOURCE': 1, + 'fastvideo/tests/performance/test_inference_performance.py: read TEST_SCOPE': 2, + 'fastvideo/tests/performance/test_inference_performance.py: write FASTVIDEO_STAGE_LOGGING': 3, + 'fastvideo/tests/performance/test_inference_performance_identity.py: write FASTVIDEO_ATTENTION_BACKEND': 5, + 'fastvideo/tests/performance/test_inference_performance_identity.py: write FASTVIDEO_CONTAINER_IMAGE_REF': 2, + 'fastvideo/tests/performance/test_inference_performance_identity.py: write FASTVIDEO_FA4': 2, + 'fastvideo/tests/performance/test_inference_performance_identity.py: write FASTVIDEO_PERFORMANCE_PROFILE_VERSION': 1, + 'fastvideo/tests/performance/test_inference_performance_identity.py: write IMAGE_VERSION': 1, + 'fastvideo/tests/performance/test_inference_performance_result_schema.py: write BUILDKITE_BRANCH': 1, + 'fastvideo/tests/performance/test_inference_performance_result_schema.py: write BUILDKITE_BUILD_ID': 1, + 'fastvideo/tests/performance/test_inference_performance_result_schema.py: write BUILDKITE_BUILD_URL': 1, + 'fastvideo/tests/performance/test_inference_performance_result_schema.py: write BUILDKITE_COMMIT': 1, + 'fastvideo/tests/performance/test_inference_performance_result_schema.py: write BUILDKITE_JOB_ID': 1, + 'fastvideo/tests/performance/test_inference_performance_result_schema.py: write BUILDKITE_PULL_REQUEST': 1, + 'fastvideo/tests/performance/test_inference_performance_result_schema.py: write PERF_RUN_SOURCE': 1, + 'fastvideo/tests/performance/test_inference_performance_result_schema.py: write TEST_SCOPE': 1, + 'fastvideo/tests/ssim/bootstrap_references.py: read ': 3, + 'fastvideo/tests/ssim/ci_runner.py: whole-environ': 1, + 'fastvideo/tests/ssim/conftest.py: read ': 1, + 'fastvideo/tests/ssim/conftest.py: read FASTVIDEO_SSIM_MODEL_ID': 1, + 'fastvideo/tests/ssim/conftest.py: read FASTVIDEO_SSIM_SKIP_REFERENCE_DOWNLOAD': 1, + 'fastvideo/tests/ssim/conftest.py: write ': 3, + 'fastvideo/tests/ssim/inference_similarity_utils.py: read FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/ssim/inference_similarity_utils.py: write FASTVIDEO_ATTENTION_BACKEND': 3, + 'fastvideo/tests/ssim/reference_utils.py: read ': 1, + 'fastvideo/tests/ssim/reference_videos_cli.py: read ': 3, + 'fastvideo/tests/ssim/test_dreamx_world_similarity.py: read DREAMX_WORLD_AR_SSIM_MODEL_PATH': 1, + 'fastvideo/tests/ssim/test_dreamx_world_similarity.py: read DREAMX_WORLD_SSIM_MODEL_PATH': 1, + 'fastvideo/tests/ssim/test_flux_t2i_similarity.py: read FLUX_T2I_MODEL_DIR': 1, + 'fastvideo/tests/ssim/test_gamecraft_similarity.py: read GAMECRAFT_MODEL_PATH': 1, + 'fastvideo/tests/ssim/test_gamecraft_similarity.py: write FASTVIDEO_ATTENTION_BACKEND': 2, + 'fastvideo/tests/ssim/test_gen3c_similarity.py: read GEN3C_MODEL_PATH': 1, + 'fastvideo/tests/ssim/test_gen3c_similarity.py: read GEN3C_TEST_IMAGE_PATH': 1, + 'fastvideo/tests/ssim/test_gen3c_similarity.py: write FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/ssim/test_glm_image_similarity.py: read GLM_IMAGE_LOCAL_WEIGHTS_DIR': 1, + 'fastvideo/tests/ssim/test_glm_image_similarity.py: read GLM_IMAGE_MODEL_DIR': 1, + 'fastvideo/tests/ssim/test_kandinsky5_similarity.py: read FASTVIDEO_FA4': 1, + 'fastvideo/tests/ssim/test_kandinsky5_similarity.py: write FASTVIDEO_FA4': 3, + 'fastvideo/tests/ssim/test_lingbot_similarity.py: write FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/ssim/test_longcat_similarity.py: write FASTVIDEO_ATTENTION_BACKEND': 3, + 'fastvideo/tests/ssim/test_matrixgame2_similarity.py: write FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/ssim/test_matrixgame3_similarity.py: write FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/ssim/test_sd35_similarity.py: read SD35_MODEL_DIR': 1, + 'fastvideo/tests/ssim/test_zimage_similarity.py: read ZIMAGE_MODEL_DIR': 1, + 'fastvideo/tests/ssim/test_zimage_similarity.py: read ZIMAGE_MODEL_REVISION': 1, + 'fastvideo/tests/stages/_denoising_fixtures.py: write FASTVIDEO_CFG_GATE_STEP': 2, + 'fastvideo/tests/stages/test_kandinsky5_dmd_stage_backend_engages.py: write FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/stages/test_minimax_h3_encoding_offload.py: write FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/train/methods/grad_norm_regression.py: read ': 1, + 'fastvideo/tests/train/methods/test_cosmos_finetune.py: write MASTER_ADDR': 1, + 'fastvideo/tests/train/methods/test_cosmos_finetune.py: write MASTER_PORT': 1, + 'fastvideo/tests/train/methods/test_ltx2_finetune.py: write MASTER_ADDR': 1, + 'fastvideo/tests/train/methods/test_ltx2_finetune.py: write MASTER_PORT': 1, + 'fastvideo/tests/train/methods/test_matrixgame2_finetune.py: write MASTER_ADDR': 1, + 'fastvideo/tests/train/methods/test_matrixgame2_finetune.py: write MASTER_PORT': 1, + 'fastvideo/tests/train/methods/test_minimax_h3_finetune.py: whole-environ': 2, + 'fastvideo/tests/train/methods/test_streaming_long_tuning.py: write MASTER_ADDR': 1, + 'fastvideo/tests/train/methods/test_streaming_long_tuning.py: write MASTER_PORT': 1, + 'fastvideo/tests/train/methods/test_wan_causal_cd.py: write MASTER_ADDR': 1, + 'fastvideo/tests/train/methods/test_wan_causal_cd.py: write MASTER_PORT': 1, + 'fastvideo/tests/train/methods/test_wan_causal_dfsft.py: write MASTER_ADDR': 1, + 'fastvideo/tests/train/methods/test_wan_causal_dfsft.py: write MASTER_PORT': 1, + 'fastvideo/tests/train/methods/test_wan_causal_dfsft_framewise.py: write MASTER_ADDR': 1, + 'fastvideo/tests/train/methods/test_wan_causal_dfsft_framewise.py: write MASTER_PORT': 1, + 'fastvideo/tests/train/methods/test_wan_causal_tfsft.py: write MASTER_ADDR': 1, + 'fastvideo/tests/train/methods/test_wan_causal_tfsft.py: write MASTER_PORT': 1, + 'fastvideo/tests/train/methods/test_wan_finetune.py: write MASTER_ADDR': 1, + 'fastvideo/tests/train/methods/test_wan_finetune.py: write MASTER_PORT': 1, + 'fastvideo/tests/train/models/test_kandinsky5_qat_attention_engages.py: write FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/train/models/test_kandinsky5_qat_attention_engages.py: write MASTER_ADDR': 1, + 'fastvideo/tests/train/models/test_kandinsky5_qat_attention_engages.py: write MASTER_PORT': 1, + 'fastvideo/tests/train/models/test_load_cosmos.py: write MASTER_ADDR': 1, + 'fastvideo/tests/train/models/test_load_cosmos.py: write MASTER_PORT': 1, + 'fastvideo/tests/train/models/test_load_hunyuan.py: write MASTER_ADDR': 1, + 'fastvideo/tests/train/models/test_load_hunyuan.py: write MASTER_PORT': 1, + 'fastvideo/tests/train/models/test_load_kandinsky5.py: write FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/train/models/test_load_kandinsky5.py: write MASTER_ADDR': 1, + 'fastvideo/tests/train/models/test_load_kandinsky5.py: write MASTER_PORT': 1, + 'fastvideo/tests/train/models/test_load_longcat.py: write MASTER_ADDR': 1, + 'fastvideo/tests/train/models/test_load_longcat.py: write MASTER_PORT': 1, + 'fastvideo/tests/train/models/test_load_ltx2.py: write MASTER_ADDR': 1, + 'fastvideo/tests/train/models/test_load_ltx2.py: write MASTER_PORT': 1, + 'fastvideo/tests/train/models/test_load_matrixgame2.py: write MASTER_ADDR': 1, + 'fastvideo/tests/train/models/test_load_matrixgame2.py: write MASTER_PORT': 1, + 'fastvideo/tests/train/models/test_load_wan.py: write MASTER_ADDR': 1, + 'fastvideo/tests/train/models/test_load_wan.py: write MASTER_PORT': 1, + 'fastvideo/tests/train/models/test_load_wan_causal.py: write MASTER_ADDR': 1, + 'fastvideo/tests/train/models/test_load_wan_causal.py: write MASTER_PORT': 1, + 'fastvideo/tests/training/VSA/test_training_loss_VSA.py: write FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/training/VSA/test_training_loss_VSA.py: write WANDB_MODE': 1, + 'fastvideo/tests/training/Vanilla/mfu_calculation.py: read PYTHONPATH': 1, + 'fastvideo/tests/training/Vanilla/mfu_calculation.py: write MASTER_ADDR': 1, + 'fastvideo/tests/training/Vanilla/mfu_calculation.py: write MASTER_PORT': 1, + 'fastvideo/tests/training/Vanilla/mfu_calculation.py: write PYTHONPATH': 1, + 'fastvideo/tests/training/Vanilla/mfu_calculation.py: write WANDB_MODE': 1, + 'fastvideo/tests/training/Vanilla/test_training_loss.py: write MASTER_ADDR': 1, + 'fastvideo/tests/training/Vanilla/test_training_loss.py: write MASTER_PORT': 1, + 'fastvideo/tests/training/Vanilla/test_training_loss.py: write WANDB_MODE': 1, + 'fastvideo/tests/training/distill/test_anyflow_smoke.py: whole-environ': 2, + 'fastvideo/tests/training/distill/test_distill_dmd.py: write MASTER_ADDR': 1, + 'fastvideo/tests/training/distill/test_distill_dmd.py: write MASTER_PORT': 1, + 'fastvideo/tests/training/distill/test_distill_dmd.py: write WANDB_MODE': 1, + 'fastvideo/tests/training/lora/test_lora_training.py: write MASTER_ADDR': 1, + 'fastvideo/tests/training/lora/test_lora_training.py: write MASTER_PORT': 1, + 'fastvideo/tests/training/lora/test_lora_training.py: write WANDB_MODE': 1, + 'fastvideo/tests/training/self-forcing/test_self_forcing.py: write MASTER_ADDR': 1, + 'fastvideo/tests/training/self-forcing/test_self_forcing.py: write MASTER_PORT': 1, + 'fastvideo/tests/training/self-forcing/test_self_forcing.py: write WANDB_MODE': 1, + 'fastvideo/tests/transformers/test_cosmos.py: write MASTER_ADDR': 1, + 'fastvideo/tests/transformers/test_cosmos.py: write MASTER_PORT': 1, + 'fastvideo/tests/transformers/test_cosmos2_5.py: write MASTER_ADDR': 1, + 'fastvideo/tests/transformers/test_cosmos2_5.py: write MASTER_PORT': 1, + 'fastvideo/tests/transformers/test_flux.py: read FLUX_TRANSFORMER_PATH': 1, + 'fastvideo/tests/transformers/test_flux.py: write FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/transformers/test_flux.py: write MASTER_ADDR': 1, + 'fastvideo/tests/transformers/test_flux.py: write MASTER_PORT': 1, + 'fastvideo/tests/transformers/test_hunyuangamecraft.py: write DISABLE_SP': 1, + 'fastvideo/tests/transformers/test_hunyuangamecraft.py: write MASTER_ADDR': 1, + 'fastvideo/tests/transformers/test_hunyuangamecraft.py: write MASTER_PORT': 1, + 'fastvideo/tests/transformers/test_hunyuangamecraft.py: write TORCHDYNAMO_DISABLE': 1, + 'fastvideo/tests/transformers/test_hyworld.py: read FASTVIDEO_ATTENTION_BACKEND': 1, + 'fastvideo/tests/transformers/test_hyworld.py: write MASTER_ADDR': 1, + 'fastvideo/tests/transformers/test_hyworld.py: write MASTER_PORT': 1, + 'fastvideo/tests/transformers/test_minimax_h3_fusion_routing.py: write FASTVIDEO_ATTENTION_BACKEND': 2, + 'fastvideo/tests/transformers/test_minimax_h3_fusion_routing.py: write FASTVIDEO_FA4': 1, + 'fastvideo/tests/transformers/test_minimax_h3_fusion_routing.py: write LOCAL_RANK': 2, + 'fastvideo/tests/transformers/test_minimax_h3_fusion_routing.py: write MASTER_ADDR': 2, + 'fastvideo/tests/transformers/test_minimax_h3_fusion_routing.py: write MASTER_PORT': 2, + 'fastvideo/tests/transformers/test_minimax_h3_fusion_routing.py: write RANK': 2, + 'fastvideo/tests/transformers/test_minimax_h3_fusion_routing.py: write WORLD_SIZE': 2, + 'fastvideo/tests/transformers/test_wanvideo.py: write MASTER_ADDR': 1, + 'fastvideo/tests/transformers/test_wanvideo.py: write MASTER_PORT': 1, + 'fastvideo/tests/vaes/test_hunyuan15_vae.py: write MASTER_ADDR': 1, + 'fastvideo/tests/vaes/test_hunyuan15_vae.py: write MASTER_PORT': 1, + 'fastvideo/tests/vaes/test_wan_vae.py: write MASTER_ADDR': 1, + 'fastvideo/tests/vaes/test_wan_vae.py: write MASTER_PORT': 1, + 'fastvideo/tests/worker/test_gpu_worker.py: write FASTVIDEO_NVTX_PROFILE': 2, + 'fastvideo/tests/worker/test_gpu_worker.py: write LOCAL_RANK': 1, + 'fastvideo/train/entrypoint/dcp_to_diffusers.py: write ': 1, + 'fastvideo/train/entrypoint/train.py: write FASTVIDEO_ATTENTION_BACKEND': 2, + 'fastvideo/training/self_forcing_distillation_pipeline.py: read FASTVIDEO_FSDP2_AUTOWRAP': 1, + 'fastvideo/utils.py: read ': 5, + 'fastvideo/utils.py: read FASTVIDEO_WORKER_MULTIPROC_METHOD': 1, + 'fastvideo/utils.py: write ': 1, + 'fastvideo/utils.py: write FASTVIDEO_WORKER_MULTIPROC_METHOD': 1, + 'fastvideo/worker/gpu_worker.py: write LOCAL_RANK': 1, + 'fastvideo/worker/gpu_worker.py: write NCCL_ASYNC_ERROR_HANDLING': 1, + 'fastvideo/worker/gpu_worker.py: write RANK': 1, + 'fastvideo/worker/gpu_worker.py: write TORCH_NCCL_AVOID_RECORD_STREAMS': 1, + 'fastvideo/worker/gpu_worker.py: write WORLD_SIZE': 1, + 'fastvideo/worker/ray_distributed_executor.py: read ': 2, + 'fastvideo/worker/ray_distributed_executor.py: read RAY_USAGE_STATS_ENABLED': 1, + 'fastvideo/worker/ray_distributed_executor.py: whole-environ': 1, + 'fastvideo/worker/ray_distributed_executor.py: write RAY_USAGE_STATS_ENABLED': 1, + 'fastvideo/worker/ray_env.py: import-time-read FASTVIDEO_CONFIG_ROOT': 1, + 'fastvideo/worker/ray_env.py: read ': 1, + 'fastvideo/worker/ray_utils.py: whole-environ': 1, + 'fastvideo/worker/worker_base.py: read ': 1, + 'fastvideo/worker/worker_base.py: write ': 1, +} + + +def load_registry() -> ModuleType: + """Load fastvideo/envs.py as a standalone module, without importing the fastvideo package.""" + spec = importlib.util.spec_from_file_location("_fastvideo_envs_registry", REGISTRY_PATH) + assert spec is not None and spec.loader is not None + registry = importlib.util.module_from_spec(spec) + spec.loader.exec_module(registry) + return registry + + +def is_allowlisted(name: str) -> bool: + for pattern in EXTERNAL_ALLOWLIST: + if pattern.endswith("*") and name.startswith(pattern[:-1]): + return True + if name == pattern: + return True + return False + + +def _literal(node: ast.AST | None) -> str: + if isinstance(node, ast.Constant) and isinstance(node.value, str): + return node.value + return "" + + +class EnvAccessScanner: + """Find environment accesses and registry-field uses in one module. + + ``violations`` holds (kind, name, line) tuples; ``registry_reads`` counts + ``envs.X.get()`` and ``envs.X.is_set()`` calls by variable name. + """ + + def __init__(self, tree: ast.Module, registry_names: set[str]) -> None: + self.registry_names = registry_names + self.violations: list[tuple[str, str, int]] = [] + self.registry_reads: Counter[str] = Counter() + self.parents: dict[ast.AST, ast.AST] = {} + self._collect_aliases(tree) + self._visit(tree, in_function=False) + + def _collect_aliases(self, tree: ast.Module) -> None: + """Record the local names bound to os, os.environ, os.getenv-like functions, and fastvideo.envs.""" + self.os_names: set[str] = set() + self.environ_names: set[str] = set() + self.getenv_names: set[str] = set() + self.setenv_names: set[str] = set() + self.envs_module_names: set[str] = set() + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + if alias.name == "os" or alias.name.startswith("os."): + self.os_names.add(alias.asname or "os") + if alias.name == "fastvideo.envs" and alias.asname: + self.envs_module_names.add(alias.asname) + elif isinstance(node, ast.ImportFrom): + for alias in node.names: + local = alias.asname or alias.name + if node.module == "os": + if alias.name in ("environ", "environb"): + self.environ_names.add(local) + elif alias.name in ("getenv", "getenvb"): + self.getenv_names.add(local) + elif alias.name in ("putenv", "unsetenv"): + self.setenv_names.add(local) + elif node.module == "fastvideo" and alias.name == "envs": + self.envs_module_names.add(local) + + def _is_os_attr(self, node: ast.AST, attrs: tuple[str, ...]) -> bool: + return (isinstance(node, ast.Attribute) and node.attr in attrs and isinstance(node.value, ast.Name) + and node.value.id in self.os_names) + + def _is_environ(self, node: ast.AST) -> bool: + return self._is_os_attr(node, ("environ", "environb")) or (isinstance(node, ast.Name) + and node.id in self.environ_names) + + def _is_envs_module(self, node: ast.AST) -> bool: + return ((isinstance(node, ast.Name) and node.id in self.envs_module_names) + or (isinstance(node, ast.Attribute) and node.attr == "envs")) + + def _visit(self, node: ast.AST, in_function: bool) -> None: + """Walk the tree, tracking whether each node runs inside a function body.""" + self._check(node, in_function) + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): + # Decorators, defaults, and annotations run when the function is defined. + outside = [*getattr(node, "decorator_list", []), *node.args.defaults, *node.args.kw_defaults] + for child in outside: + if child is not None: + self.parents[child] = node + self._visit(child, in_function) + body = node.body if isinstance(node.body, list) else [node.body] + for child in body: + self.parents[child] = node + self._visit(child, True) + return + for child in ast.iter_child_nodes(node): + self.parents[child] = node + self._visit(child, in_function) + + def _check(self, node: ast.AST, in_function: bool) -> None: + parent = self.parents.get(node) + if self._is_environ(node): + self._check_environ_use(node, parent) + elif isinstance(node, ast.Call): + self._check_call(node) + elif isinstance(node, ast.Attribute) and node.attr in self.registry_names and self._is_envs_module(node.value): + self._check_registry_use(node, parent, in_function) + + def _check_environ_use(self, node: ast.AST, parent: ast.AST | None) -> None: + line = node.lineno # type: ignore[attr-defined] + if isinstance(parent, ast.Attribute) and isinstance(self.parents.get(parent), ast.Call): + call = self.parents[parent] + assert isinstance(call, ast.Call) + if call.func is parent and parent.attr == "get": + self.violations.append(("read", _literal(call.args[0] if call.args else None), line)) + return + if call.func is parent and parent.attr in ("setdefault", "pop"): + self.violations.append(("write", _literal(call.args[0] if call.args else None), line)) + return + if isinstance(parent, ast.Subscript) and parent.value is node: + kind = "read" if isinstance(parent.ctx, ast.Load) else "write" + self.violations.append((kind, _literal(parent.slice), line)) + return + if (isinstance(parent, ast.Compare) and len(parent.ops) == 1 and isinstance(parent.ops[0], (ast.In, ast.NotIn)) + and parent.comparators[0] is node): + self.violations.append(("read", _literal(parent.left), line)) + return + self.violations.append(("whole-environ", "", line)) + + def _check_call(self, node: ast.Call) -> None: + func = node.func + name = _literal(node.args[0] if node.args else None) + if self._is_os_attr(func, ("getenv", "getenvb")) or (isinstance(func, ast.Name) + and func.id in self.getenv_names): + self.violations.append(("read", name, node.lineno)) + elif (self._is_os_attr(func, ("putenv", "unsetenv")) + or (isinstance(func, ast.Name) and func.id in self.setenv_names) + or (isinstance(func, ast.Attribute) and func.attr in ("setenv", "delenv"))): + self.violations.append(("write", name, node.lineno)) + + def _check_registry_use(self, node: ast.Attribute, parent: ast.AST | None, in_function: bool) -> None: + # Only a call counts: ``getter = envs.X.get`` neither reads the variable + # nor uses the field through a method. + grandparent = self.parents.get(parent) if parent is not None else None + if not (isinstance(parent, ast.Attribute) and parent.attr in REGISTRY_METHODS + and isinstance(grandparent, ast.Call) and grandparent.func is parent): + self.violations.append(("bare-field", node.attr, node.lineno)) + return + if parent.attr in REGISTRY_READ_METHODS: + self.registry_reads[node.attr] += 1 + if not in_function: + self.violations.append(("import-time-read", node.attr, node.lineno)) + + +def scanned_files() -> list[Path]: + return sorted(path for path in PACKAGE_ROOT.rglob("*.py") + if path != REGISTRY_PATH and not any(excluded in path.parents for excluded in EXCLUDED_DIRS)) + + +def collect_violations(registry: ModuleType) -> tuple[dict[str, list[int]], Counter[str]]: + """Return every violation key with its line numbers, plus registry read counts.""" + registry_names = set(registry.environment_variables) + found: dict[str, list[int]] = {} + reads: Counter[str] = Counter() + for path in scanned_files(): + relative = path.relative_to(REPO_ROOT).as_posix() + scanner = EnvAccessScanner(ast.parse(path.read_text(encoding="utf-8"), filename=relative), registry_names) + reads.update(scanner.registry_reads) + for kind, name, line in scanner.violations: + if kind == "read" and is_allowlisted(name): + continue + key = f"{relative}: {kind} {name}".rstrip() + found.setdefault(key, []).append(line) + + registry_path = REGISTRY_PATH.relative_to(REPO_ROOT).as_posix() + for name in registry.environment_variables: + if not re.fullmatch(r"FASTVIDEO_[A-Z0-9_]+", name): + found.setdefault(f"{registry_path}: prefix {name}", []).append(0) + if reads[name] == 0: + found.setdefault(f"{registry_path}: unread {name}", []).append(0) + return found, reads + + +def test_env_access_follows_policy(): + found, _ = collect_violations(load_registry()) + counts = Counter({key: len(lines) for key, lines in found.items()}) + known = Counter(KNOWN_VIOLATIONS) + + new = counts - known + fixed = known - counts + messages = [] + if new: + lines = [f" {key} (lines {found[key]})" for key in sorted(new)] + messages.append(f"New environment-variable policy violations. See {POLICY_DOC} for the rule and the fix:\n" + + "\n".join(lines)) + if fixed: + lines = [f" {key}" for key in sorted(fixed)] + messages.append("These KNOWN_VIOLATIONS entries are fixed; delete them from " + "fastvideo/tests/contract/test_env_policy.py:\n" + "\n".join(lines)) + assert not messages, "\n\n".join(messages) + + +def test_registry_method_counts_only_when_called(): + """An uncalled ``envs.NAME.get`` is a bare field, not a read.""" + source = ("import fastvideo.envs as envs\n" + "def f():\n" + " getter = envs.FASTVIDEO_FA4.get\n" + " return envs.FASTVIDEO_FA4.get()\n") + scanner = EnvAccessScanner(ast.parse(source), {"FASTVIDEO_FA4"}) + assert scanner.violations == [("bare-field", "FASTVIDEO_FA4", 3)] + assert scanner.registry_reads["FASTVIDEO_FA4"] == 1 + + +def test_registry_entries_have_category_and_description(): + registry = load_registry() + problems = [] + for name, field in registry.environment_variables.items(): + if field.category not in registry.CATEGORIES: + problems.append(f"{name}: category {field.category!r} is not in envs.CATEGORIES") + if not field.doc.strip(): + problems.append(f"{name}: empty description") + assert not problems, f"Registry entries break {POLICY_DOC}:\n" + "\n".join(problems) + + +def _render_default(field) -> str: + if callable(field.default): + return "computed" + if field.default is None: + return "unset" + return f"`{field.format(field.default)}`" if field.format(field.default) else '`""`' + + +def _escape(text: str) -> str: + return text.replace("|", "\\|").replace("*", "\\*").replace("<", "<").replace(">", ">") + + +def render_env_table(registry: ModuleType) -> str: + """Render the registry as an aligned Markdown table, in declaration order.""" + rows = [["Variable", "Type", "Default", "Category", "Description"]] + for name, field in registry.environment_variables.items(): + rows.append([f"`{name}`", field.type_name, _render_default(field), field.category, _escape(field.doc)]) + widths = [max(len(row[column]) for row in rows) for column in range(len(rows[0]))] + + def render_row(cells: list[str]) -> str: + return "| " + " | ".join(cell.ljust(width) for cell, width in zip(cells, widths)) + " |" + + lines = [render_row(rows[0]), render_row(["-" * width for width in widths])] + lines.extend(render_row(row) for row in rows[1:]) + return "\n".join(lines) + + +def _split_doc(text: str) -> tuple[str, str, str]: + """Split the policy doc into the text before the generated table, the table, and the text after it.""" + before, found_begin, rest = text.partition(DOC_TABLE_BEGIN) + table, found_end, after = rest.partition(DOC_TABLE_END) + assert found_begin and found_end, f"{POLICY_DOC} must contain the generated-table markers" + return before + found_begin + "\n", table.strip("\n"), "\n" + found_end + after + + +def test_env_doc_table_matches_registry(): + _, table, _ = _split_doc(DOC_PATH.read_text(encoding="utf-8")) + assert table == render_env_table(load_registry()), ( + f"The table in {POLICY_DOC} is out of date. Run `python fastvideo/tests/contract/test_env_policy.py`.") + + +if __name__ == "__main__": + head, _, tail = _split_doc(DOC_PATH.read_text(encoding="utf-8")) + DOC_PATH.write_text(head + render_env_table(load_registry()) + tail, encoding="utf-8") + print(f"Updated {POLICY_DOC}") diff --git a/fastvideo/tests/stages/_denoising_fixtures.py b/fastvideo/tests/stages/_denoising_fixtures.py index 2cc589c99..6ad590248 100644 --- a/fastvideo/tests/stages/_denoising_fixtures.py +++ b/fastvideo/tests/stages/_denoising_fixtures.py @@ -113,9 +113,9 @@ def _patch_denoising_module(monkeypatch, cfg_gate_step): import fastvideo.pipelines.stages.denoising as denoising from fastvideo.pipelines.stages.base import PipelineStage - # envs.py evaluates FASTVIDEO_CFG_GATE_STEP lazily via __getattr__, so the - # stage sees monkeypatched values without reloading the module. - assert denoising.envs.FASTVIDEO_CFG_GATE_STEP == expected_gate_step + # envs.FASTVIDEO_CFG_GATE_STEP.get() reads the environment on each call, so + # the stage sees monkeypatched values without reloading the module. + assert denoising.envs.FASTVIDEO_CFG_GATE_STEP.get() == expected_gate_step logger = RecordingLogger() monkeypatch.setattr(denoising, "logger", logger) diff --git a/fastvideo/train/methods/base.py b/fastvideo/train/methods/base.py index 5168313c5..92faf4b02 100644 --- a/fastvideo/train/methods/base.py +++ b/fastvideo/train/methods/base.py @@ -274,7 +274,7 @@ class TrainingMethod(torch.nn.Module, ABC): def _infer_attn_kind(self) -> Literal["dense", "vsa"]: """Derive metadata mode from the student's resolved backend.""" - backend = (self.student.attention_backend_name or envs.FASTVIDEO_ATTENTION_BACKEND) + backend = (self.student.attention_backend_name or envs.FASTVIDEO_ATTENTION_BACKEND.get()) if backend == "VIDEO_SPARSE_ATTN": return "vsa" return "dense" diff --git a/fastvideo/train/models/ltx2/ltx2.py b/fastvideo/train/models/ltx2/ltx2.py index 9f891019e..c36fe200b 100644 --- a/fastvideo/train/models/ltx2/ltx2.py +++ b/fastvideo/train/models/ltx2/ltx2.py @@ -525,7 +525,7 @@ class LTX2Model(WanModel): return training_batch def _build_attention_metadata(self, training_batch: TrainingBatch) -> TrainingBatch: - if envs.FASTVIDEO_ATTENTION_BACKEND in ("VIDEO_SPARSE_ATTN", "VMOBA_ATTN"): + if envs.FASTVIDEO_ATTENTION_BACKEND.get() in ("VIDEO_SPARSE_ATTN", "VMOBA_ATTN"): raise NotImplementedError("LTX2Model does not support VSA/VMOBA attention backends") training_batch.attn_metadata = None return training_batch diff --git a/fastvideo/train/models/wan/wan.py b/fastvideo/train/models/wan/wan.py index 7737639b3..aaf74e98b 100644 --- a/fastvideo/train/models/wan/wan.py +++ b/fastvideo/train/models/wan/wan.py @@ -490,7 +490,7 @@ class WanModel(ModelBase): assert latents_shape is not None assert training_batch.timesteps is not None - attention_backend = (self.attention_backend_name or envs.FASTVIDEO_ATTENTION_BACKEND) + attention_backend = (self.attention_backend_name or envs.FASTVIDEO_ATTENTION_BACKEND.get()) if attention_backend == "VIDEO_SPARSE_ATTN": if (not is_vsa_available() or VideoSparseAttentionMetadataBuilder is None): raise ImportError("FASTVIDEO_ATTENTION_BACKEND is " diff --git a/fastvideo/training/distillation_pipeline.py b/fastvideo/training/distillation_pipeline.py index 2f9e86009..524bffe79 100644 --- a/fastvideo/training/distillation_pipeline.py +++ b/fastvideo/training/distillation_pipeline.py @@ -1324,7 +1324,7 @@ class DistillationPipeline(TrainingPipeline): disable=self.local_rank > 0, ) - use_vsa = vsa_available and envs.FASTVIDEO_ATTENTION_BACKEND == "VIDEO_SPARSE_ATTN" + use_vsa = vsa_available and envs.FASTVIDEO_ATTENTION_BACKEND.get() == "VIDEO_SPARSE_ATTN" for step in range(self.init_steps + 1, self.training_args.max_train_steps + 1): if step % 5 == 0: gc.collect() @@ -1506,7 +1506,7 @@ class DistillationPipeline(TrainingPipeline): if self.training_args.use_ema and self.is_ema_ready(): self.save_ema_weights(self.training_args.output_dir, self.training_args.max_train_steps) - if envs.FASTVIDEO_TORCH_PROFILER_DIR: + if envs.FASTVIDEO_TORCH_PROFILER_DIR.get(): logger.info("Stopping profiler...") self.profiler_controller.stop() logger.info("Profiler stopped.") diff --git a/fastvideo/training/self_forcing_distillation_pipeline.py b/fastvideo/training/self_forcing_distillation_pipeline.py index 9e6b0b26f..bbc5a44d6 100644 --- a/fastvideo/training/self_forcing_distillation_pipeline.py +++ b/fastvideo/training/self_forcing_distillation_pipeline.py @@ -833,7 +833,7 @@ class SelfForcingDistillationPipeline(DistillationPipeline): disable=self.local_rank > 0, ) - use_vsa = vsa_available and envs.FASTVIDEO_ATTENTION_BACKEND == "VIDEO_SPARSE_ATTN" + use_vsa = vsa_available and envs.FASTVIDEO_ATTENTION_BACKEND.get() == "VIDEO_SPARSE_ATTN" for step in range(self.init_steps + 1, self.training_args.max_train_steps + 1): start_time = time.perf_counter() if use_vsa: @@ -1008,7 +1008,7 @@ class SelfForcingDistillationPipeline(DistillationPipeline): if self.training_args.use_ema and self.is_ema_ready(): self.save_ema_weights(self.training_args.output_dir, self.training_args.max_train_steps) - if envs.FASTVIDEO_TORCH_PROFILER_DIR: + if envs.FASTVIDEO_TORCH_PROFILER_DIR.get(): logger.info("Stopping profiler...") self.profiler_controller.stop() logger.info("Profiler stopped.") diff --git a/fastvideo/training/training_pipeline.py b/fastvideo/training/training_pipeline.py index 97c66ecf7..f5a3939ad 100644 --- a/fastvideo/training/training_pipeline.py +++ b/fastvideo/training/training_pipeline.py @@ -348,7 +348,7 @@ class TrainingPipeline(LoRAPipeline, ABC): current_vsa_sparsity = training_batch.current_vsa_sparsity assert latents_shape is not None assert training_batch.timesteps is not None - if envs.FASTVIDEO_ATTENTION_BACKEND == "VIDEO_SPARSE_ATTN": + if envs.FASTVIDEO_ATTENTION_BACKEND.get() == "VIDEO_SPARSE_ATTN": if not vsa_available: raise ImportError("FASTVIDEO_ATTENTION_BACKEND is set to VIDEO_SPARSE_ATTN, " "but fastvideo_kernel is not correctly installed or detected. " @@ -361,7 +361,7 @@ class TrainingPipeline(LoRAPipeline, ABC): VSA_sparsity=current_vsa_sparsity, device=get_local_torch_device(), cache_tile_buf=self.training_args.VSA_cache_tile_buf) - elif envs.FASTVIDEO_ATTENTION_BACKEND == "VMOBA_ATTN": + elif envs.FASTVIDEO_ATTENTION_BACKEND.get() == "VMOBA_ATTN": if not vmoba_available: raise ImportError("FASTVIDEO_ATTENTION_BACKEND is set to VMOBA_ATTN, " "but fastvideo_kernel (or flash_attn>=2.7.4) is not correctly installed.") @@ -389,7 +389,8 @@ class TrainingPipeline(LoRAPipeline, ABC): return training_batch def _transformer_forward_and_compute_loss(self, training_batch: TrainingBatch) -> TrainingBatch: - if vsa_available and envs.FASTVIDEO_ATTENTION_BACKEND == "VIDEO_SPARSE_ATTN" or vmoba_available and envs.FASTVIDEO_ATTENTION_BACKEND == "VMOBA_ATTN": + if vsa_available and envs.FASTVIDEO_ATTENTION_BACKEND.get( + ) == "VIDEO_SPARSE_ATTN" or vmoba_available and envs.FASTVIDEO_ATTENTION_BACKEND.get() == "VMOBA_ATTN": assert training_batch.attn_metadata is not None else: assert training_batch.attn_metadata is None @@ -642,7 +643,7 @@ class TrainingPipeline(LoRAPipeline, ABC): self.training_args.max_train_steps, self.optimizer, self.train_dataloader, self.lr_scheduler, self.noise_random_generator) - if envs.FASTVIDEO_TORCH_PROFILER_DIR: + if envs.FASTVIDEO_TORCH_PROFILER_DIR.get(): logger.info("Stopping profiler...") self.profiler_controller.stop() logger.info("Profiler stopped.") diff --git a/fastvideo/utils.py b/fastvideo/utils.py index fbf3db6c1..441ac3fcd 100644 --- a/fastvideo/utils.py +++ b/fastvideo/utils.py @@ -64,7 +64,7 @@ def find_nccl_library() -> str: After importing `torch`, `libnccl.so.2` or `librccl.so.1` can be found by `ctypes` automatically. """ - so_file = envs.FASTVIDEO_NCCL_SO_PATH + so_file = envs.FASTVIDEO_NCCL_SO_PATH.get() # manually load the nccl library if so_file: @@ -87,7 +87,7 @@ def find_hccl_library() -> str: After importing `torch`, `libhccl.so` can be found by `ctypes` automatically. """ - so_file = envs.HCCL_SO_PATH + so_file = envs.HCCL_SO_PATH.get() # manually load the nccl library if so_file: @@ -1127,7 +1127,7 @@ def _append_to_memory_trace(message: str, log_file_path: str | os.PathLike[str] # TODO(xingyu): add adopted message for this def get_ip() -> str: - host_ip = envs.FASTVIDEO_HOST_IP + host_ip = envs.FASTVIDEO_HOST_IP.get() if host_ip: return host_ip @@ -1169,7 +1169,7 @@ def test_loopback_bind(address: str, family: socket.AddressFamily) -> bool: def get_loopback_ip() -> str: - loopback_ip = envs.FASTVIDEO_LOOPBACK_IP + loopback_ip = envs.FASTVIDEO_LOOPBACK_IP.get() if loopback_ip: return loopback_ip @@ -1253,7 +1253,7 @@ def get_mp_context() -> BaseContext: FASTVIDEO_WORKER_MULTIPROC_METHOD. """ force_spawn() - mp_method = envs.FASTVIDEO_WORKER_MULTIPROC_METHOD + mp_method = envs.FASTVIDEO_WORKER_MULTIPROC_METHOD.get() return multiprocessing.get_context(mp_method) diff --git a/fastvideo/worker/gpu_worker.py b/fastvideo/worker/gpu_worker.py index 3e6d18250..8a22cdf73 100644 --- a/fastvideo/worker/gpu_worker.py +++ b/fastvideo/worker/gpu_worker.py @@ -16,7 +16,7 @@ logger = init_logger(__name__) def _log_cuda_device_uuid(rank: int, device: torch.device) -> None: """Record an NVIDIA worker UUID when external NVTX profiling is enabled.""" - if not envs.FASTVIDEO_NVTX_PROFILE: + if not envs.FASTVIDEO_NVTX_PROFILE.get(): return device_uuid = torch.cuda.get_device_properties(device).uuid logger.info("Worker %d CUDA device UUID: GPU-%s", rank, device_uuid, local_main_process_only=False) diff --git a/fastvideo/worker/multiproc_executor.py b/fastvideo/worker/multiproc_executor.py index 98714b3ba..5c4d31875 100644 --- a/fastvideo/worker/multiproc_executor.py +++ b/fastvideo/worker/multiproc_executor.py @@ -150,7 +150,7 @@ class MultiprocExecutor(Executor): output = responses[0]["output_batch"] logging_info = None - logging_info = responses[0]["logging_info"] if envs.FASTVIDEO_STAGE_LOGGING else None + logging_info = responses[0]["logging_info"] if envs.FASTVIDEO_STAGE_LOGGING.get() else None # Get extra dict (contains audio, peak_memory_mb, etc.) extra = responses[0].get("extra", {}) @@ -743,7 +743,7 @@ class WorkerMultiprocProc: fastvideo_args = kwargs['fastvideo_args'] output_batch = self.worker.execute_forward(forward_batch, fastvideo_args) logging_info = None - if envs.FASTVIDEO_STAGE_LOGGING: + if envs.FASTVIDEO_STAGE_LOGGING.get(): logging_info = output_batch.logging_info # result tensor shared by CUDA IPC to avoid serialization overhead result = output_batch.output diff --git a/fastvideo/worker/ray_distributed_executor.py b/fastvideo/worker/ray_distributed_executor.py index 67c576bd4..a48e9e506 100644 --- a/fastvideo/worker/ray_distributed_executor.py +++ b/fastvideo/worker/ray_distributed_executor.py @@ -114,7 +114,7 @@ class RayDistributedExecutor(Executor): def _init_workers_ray(self, placement_group: "PlacementGroup", **ray_remote_kwargs): from fastvideo.platforms import current_platform - num_gpus = envs.FASTVIDEO_RAY_PER_WORKER_GPUS + num_gpus = envs.FASTVIDEO_RAY_PER_WORKER_GPUS.get() # The remaining workers are the actual ray actors. self.workers: list[RayWorkerWrapper] = [] @@ -326,7 +326,7 @@ class RayDistributedExecutor(Executor): output = responses[0].output.cpu() logging_info = None - if envs.FASTVIDEO_STAGE_LOGGING: + if envs.FASTVIDEO_STAGE_LOGGING.get(): logging_info = responses[0].logging_info result_batch = ForwardBatch( diff --git a/fastvideo/worker/ray_env.py b/fastvideo/worker/ray_env.py index 72caa0131..b3663afbb 100644 --- a/fastvideo/worker/ray_env.py +++ b/fastvideo/worker/ray_env.py @@ -8,7 +8,7 @@ from fastvideo.logger import init_logger logger = init_logger(__name__) -CONFIG_HOME = envs.FASTVIDEO_CONFIG_ROOT +CONFIG_HOME = envs.FASTVIDEO_CONFIG_ROOT.get() # This file contains a list of env vars that should not be copied # from the driver to the Ray workers. diff --git a/mkdocs.yml b/mkdocs.yml index 3a67cb01c..80e390105 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -237,6 +237,7 @@ nav: - Pull Requests: contributing/pull_requests.md - CI/CD Architecture: contributing/ci_architecture.md - Testing: contributing/testing.md + - Environment Variables: contributing/env_vars.md - Performance Benchmarks: contributing/performance_benchmarks.md - Profiling: contributing/profiling.md - Activation Trace: contributing/activation_trace.md