Skip to content

[PyTorch][torch.compile] Make get_attention_backend traceable without graph breaks#3189

Open
pggPL wants to merge 12 commits into
NVIDIA:mainfrom
pggPL:attention_get_attention_backend_traceable
Open

[PyTorch][torch.compile] Make get_attention_backend traceable without graph breaks#3189
pggPL wants to merge 12 commits into
NVIDIA:mainfrom
pggPL:attention_get_attention_backend_traceable

Conversation

@pggPL

@pggPL pggPL commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR is part of an effort to enable torch.compile support for attention: it makes get_attention_backend traceable.

It works on current PyTorch as-is — there is no need to wait for the two related PRs I opened in PyTorch. Those only improve the remaining rough edge: pytorch/pytorch#189042 (bigger and much more important; among other things it handles symbolic scalar arguments, which with stock dynamo must stay non-symbolic in the backend-selection probe — with static scalars, the common case, everything in this PR already works) and pytorch/pytorch#189027 (small, and possibly not needed anymore since this PR reads env vars via os.environ.get, which stock dynamo already guards).

get_attention_backend currently graph-breaks under torch.compile, forcing backend selection to run as an eager island. This PR makes it fully traceable with fullgraph=True, with dynamo guards on the NVTE_* environment variables so that changing an env var triggers recompilation instead of silently reusing a stale backend selection.

Type of change

  • Documentation change (change only to the documentation, either a fix or a new content)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (see the FusedAttnBackend note below)
  • Infra/Build change
  • Code refactoring

Changes

  • Read NVTE_* env vars in get_attention_backend via os.environ.get instead of os.getenv: dynamo installs value guards on os.environ accesses, while os.getenv reads are unguarded (the stdlib-global source is skipped) and would bake a stale backend selection into the compiled graph.

  • Wrap the tex.get_fused_attn_backend pybind call in a torch.compiler.assume_constant_result helper — the result depends only on the attention configuration, not on tensor values.

  • Mark get_device_compute_capability and get_cudnn_version with assume_constant_result (pybind/CUDA property calls are not traceable, and their results are constant for a process).

  • Use a no-op logger when compiling (logging.Logger methods graph-break in dynamo) and skip the debug-log blocks that evaluate int()/str() on pybind enum values during tracing. Backend-selection debug logs are unchanged in eager mode.

  • Add test_get_attention_backend_traceable to tests/pytorch/test_torch_compile.py: compiles a function calling get_attention_backend with fullgraph=True (any graph break fails the test) and flips NVTE_FUSED_ATTN/NVTE_UNFUSED_ATTN/NVTE_FLASH_ATTN to verify the guards trigger recompilation and the result keeps matching eager.

  • FusedAttnBackend (in pytorch/cpp_extensions/fused_attn.py) is now a python IntEnum generated at import time from tex.NVTE_Fused_Attn_Backend.__members__, replacing the previous str->pybind-enum dict, and all direct python-side uses of the pybind enum are replaced with it. Rationale: the pybind enum is not traceable by dynamo (C __eq__), and a pybind enum baked through assume_constant_result produces guards dynamo cannot evaluate when compared against module-level enum values (hard crash when cuDNN rejects a config). The transition follows the pattern used for constants.DType: explicit members pinned to the C values with an import-time sync assert, an __eq__ override so mixed comparisons with tex.NVTE_Fused_Attn_Backend stay equivalent regardless of the pybind11 version, and a cast() classmethod; fused_attn_fwd/bwd normalize their backend argument through cast(), so callers still passing the pybind enum keep working. Name lookup FusedAttnBackend["FP8"] and int(...) behave as before. The remaining (minor) breaking surface is isinstance checks against the pybind enum and dict-API such as .items()/iteration; no such uses exist inside TE and none were found in Megatron-LM/NeMo.

Note: dynamo only guards os.environ keys that exist at trace time; reads of absent keys are not guarded yet (upstream PyTorch limitation).

Checklist:

  • I have read and followed the contributing guidelines
  • The functionality is complete
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

…reaks

- Read NVTE_* env vars via os.environ.get instead of os.getenv so dynamo
  installs guards on the values (os.getenv reads are not guarded and would
  bake stale backend selections into compiled graphs).
- Wrap tex.get_fused_attn_backend in a torch.compiler.assume_constant_result
  helper so the pybind call does not graph-break.
- Mark get_device_compute_capability/get_cudnn_version with
  assume_constant_result for the same reason.
- Use a no-op logger when compiling (logging.Logger methods graph-break) and
  skip debug-log blocks that call int()/str() on pybind enums.
- Add test in tests/pytorch/test_torch_compile.py checking fullgraph=True
  tracing and recompilation on NVTE_* env var changes.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
@pggPL
pggPL requested review from cyanguwa and ksivaman as code owners July 8, 2026 10:25
@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes get_attention_backend fully traceable under torch.compile(fullgraph=True) by: switching os.getenv calls to os.environ.get so dynamo installs value guards on NVTE_* env vars; wrapping the opaque pybind tex.get_fused_attn_backend call in an assume_constant_result helper; replacing the FusedAttnBackend string→pybind dict with a proper Python IntEnum that is dynamo-traceable; and substituting a _NoOpLogger for the real logger during compilation to avoid logging-induced graph breaks.

  • FusedAttnBackend is converted from a plain dict to a Python IntEnum with import-time sync assertion against the C++ NVTE_Fused_Attn_Backend enum, a cast() classmethod for backward-compat, and a custom __eq__/__ne__/__hash__ so mixed comparisons with the pybind enum remain transparent.
  • get_device_compute_capability and get_cudnn_version are decorated with assume_constant_result; _get_cudnn_version retains its lru_cache and is now the private implementation backing the public wrapper.
  • A new test test_get_attention_backend_traceable compiles the selection function with fullgraph=True, flips env vars, exercises the FP8 branch, and patches the probe to verify the baked constant drives selection.

Confidence Score: 5/5

Safe to merge. The backend-selection logic is correct in all paths; the FusedAttnBackend IntEnum mirrors the C++ enum exactly with an import-time assert as a safety net; os.environ.get guards and assume_constant_result are applied consistently; and backward compatibility is preserved through cast() normalisation in fused_attn_fwd/bwd.

All changes are in selection/dispatch logic with no tensor computation affected. The only findings are quality observations: warning/error logs are silently dropped during compilation (users lose fallback notices under torch.compile), a minor type annotation inaccuracy in ne, and three per-version env-var keys left out of the test pre-set. None of these affect correctness or introduce data-path regressions.

transformer_engine/pytorch/attention/dot_product_attention/utils.py deserves a second look regarding the _NoOpLogger swallowing warning/error log levels. tests/pytorch/test_torch_compile.py is missing NVTE_FLASH_ATTN_V2/V3/V4 pre-sets which limits dynamo guard coverage for those keys.

Important Files Changed

Filename Overview
transformer_engine/pytorch/cpp_extensions/fused_attn.py Replaces FusedAttnBackend dict with a properly-defined IntEnum; adds cast(), custom __eq__/__ne__/__hash__, and import-time sync assert; fused_attn_fwd/bwd now normalize via cast() for backward compatibility.
transformer_engine/pytorch/attention/dot_product_attention/utils.py Core change: _get_fused_attn_backend wraps the pybind call with assume_constant_result; _NoOpLogger avoids logging graph breaks; all os.getenvos.environ.get for dynamo guards. Warning/error logs are silently dropped during compilation.
transformer_engine/pytorch/utils.py Adds assume_constant_result to get_device_compute_capability; renames old get_cudnn_version to _get_cudnn_version (retains lru_cache) and adds a thin public get_cudnn_version wrapper with assume_constant_result.
tests/pytorch/test_torch_compile.py Adds test_get_attention_backend_traceable: tests fullgraph=True tracing, env-var guard firing, FP8 branch coverage, and parameter-change recompilation. Missing pre-set for NVTE_FLASH_ATTN_V2/V3/V4 variants means dynamo won't guard those keys.
transformer_engine/pytorch/attention/dot_product_attention/backends.py Mechanical replacement of all tex.NVTE_Fused_Attn_Backend.* references with FusedAttnBackend[...] equivalents; default parameter type updated to FusedAttnBackend.
transformer_engine/pytorch/attention/dot_product_attention/context_parallel.py Four mechanical replacements of tex.NVTE_Fused_Attn_Backend.NVTE_FP8 / NVTE_F16_arbitrary_seqlen with FusedAttnBackend["FP8"] / FusedAttnBackend["F16_arbitrary_seqlen"].

Reviews (8): Last reviewed commit: "[PyTorch] Address review: add info()/err..." | Re-trigger Greptile

Comment thread tests/pytorch/test_torch_compile.py Outdated
Comment thread tests/pytorch/test_torch_compile.py
Comment thread transformer_engine/pytorch/utils.py
pggPL added 9 commits July 8, 2026 13:18
Comparing the pybind enum returned through assume_constant_result against
module-level enum values generates guards dynamo cannot evaluate (crash when
the comparison is true, i.e. when cuDNN rejects the config). The wrapper now
returns a plain int, comparisons use precomputed int values, and the enum for
callers is reconstructed by a second assume_constant_result helper that is
never compared during tracing.

Also document that os.environ.get (vs os.getenv) is intentional, and drop the
guard_scalar specialization of numeric args: symbolic scalars (automatic
dynamic) now graph break at the probe instead of forcing a full recompile per
seqlen value; the test covers that path without fullgraph and checks the
selection stays correct. A second test monkeypatches tex.get_fused_attn_backend
to verify the baked result is trace-time-only and actually drives selection.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
The pybind NVTE_Fused_Attn_Backend enum is not traceable by torch.compile:
its C-implemented __eq__ cannot be traced, and a pybind enum instance baked
through assume_constant_result produces guards dynamo cannot evaluate when
compared against module-level enum values. FusedAttnBackend is now a plain
python IntEnum generated at import time from
tex.NVTE_Fused_Attn_Backend.__members__ (values always in sync with the C
enum), and all remaining direct uses of the pybind enum on the python side
are replaced with it. Name lookup (FusedAttnBackend["FP8"]) behaves the
same as with the previous dict, and the backend value never crosses into a
pybind call, so no boundary conversion is needed.

This removes the previous int-based workaround in get_attention_backend
(_fused_attn_backend_from_int and the precomputed int table): the
assume_constant_result wrapper now simply returns the IntEnum and
comparisons are traceable directly.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Mirror how constants.DType migrated off the pybind enum: explicit IntEnum
members pinned to the C values with an import-time sync assert, an __eq__
override comparing by integer value against NVTE_Fused_Attn_Backend (with
matching __ne__/__hash__) so mixed comparisons stay equivalent regardless of
the pybind11 version, and a cast() classmethod. fused_attn_fwd/bwd normalize
their fused_attention_backend argument through cast(), so external callers
still passing the pybind enum keep working.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
test_get_attention_backend_traceable_fp8 compiles the selection with
fullgraph=True for AttentionParams(fp8=True) with a DelayedScaling(fp8_dpa)
recipe, covering the FP8-only branch (run_config env reads, recipe filters,
get_fp8_te_dtype) and checks that flipping NVTE_UnfusedDPA_Emulate_FP8
recompiles and keeps matching eager.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
The is_compiling() guards around the available/selected-backend debug logs
protected int() on the pybind enum, which crashed dynamo during tracing.
With FusedAttnBackend now a python IntEnum, int() on it and str() on the
flash-attn PkgVersion both trace cleanly (verified under fullgraph=True), so
the logging blocks return to their upstream shape. The probe wrapper also
reuses FusedAttnBackend.cast() and a shorter docstring.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Merge the three get_attention_backend tests into one covering: fullgraph
tracing with the probe consulted at trace time only, env var flips (F16 and
FP8), attention-param changes, and a forced No_Backend result driving the
selection. The bitmask output now also encodes the fused sub-backend, which
previously went unchecked.

The probe wrapper takes layout/bias/mask/softmax as string keys and resolves
the pybind enums internally, so every argument is a literal or a python enum
- required for assume_constant_result(specialize_args=True) (pytorch#189042)
to derive value guards once available. Scalars must stay concrete until then:
the test pins specialize_int/float=True, because a symbolic scalar currently
graph breaks at the probe and dynamo's resume then corrupts the returned
fused backend (binds the wrapper function object instead of its result;
surfaced by the sub-backend bits, minimal repro exists).

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
The probe-argument and static-scalar comments referenced
assume_constant_result(specialize_args=True), which is not part of any
released PyTorch; describe the current behavior only.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Drop the call-counting monkeypatch and its assertions; compiled-vs-eager
output equality is what matters and already fails on stale selections.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
…tion_backend_traceable

# Conflicts:
#	tests/pytorch/test_torch_compile.py
@pggPL

pggPL commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

/te-ci pytorch

@pggPL pggPL changed the title [PyTorch] Make get_attention_backend traceable by torch.compile without graph breaks [PyTorch][torch.compile] Make get_attention_backend traceable without graph breaks Jul 10, 2026
cyanguwa
cyanguwa previously approved these changes Jul 22, 2026
…tion_backend_traceable

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

# Conflicts:
#	transformer_engine/pytorch/attention/dot_product_attention/utils.py
@pggPL

pggPL commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

/te-ci pytorch

Comment thread transformer_engine/pytorch/attention/dot_product_attention/utils.py
Comment thread transformer_engine/pytorch/cpp_extensions/fused_attn.py
shino16: _NoOpLogger does not subclass logging.Logger, so any traced
logger.info()/logger.error() call under torch.compile would raise
AttributeError. Add the two no-op methods for completeness.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants