Skip to content

flax.nnx.cond causes tracing cache misses #5512

Description

@futscdav

Using nnx.cond causes tracing cache misses (and thus prevents persistent compilation cache loads) due to creation of transient SimpleCondFn passed to jax.lax.cond. This is very inconvenient when using nnx.cond with larger modules that take a while to compile.

TRACING CACHE MISS at .../flax/nnx/transforms/transforms.py:623:19 (cond):
  never seen function:
    true_fn id=17864732047504 defined at .../repro_cond_miss.py:15
  but seen another function defined on the same line; maybe the function is
  being re-defined repeatedly, preventing caching?
W0622 10:13:43.770388 1182679 partial_eval.py:2164] TRACING CACHE MISS at .../flax/nnx/transforms/transforms.py:623:19 (cond):
  never seen function:
    false_fn id=17864737580928 defined at .../repro_cond_miss.py:19
  but seen another function defined on the same line; maybe the function is
  being re-defined repeatedly, preventing caching?
W0622 10:13:43.771588 1182679 dispatch.py:204] Finished tracing cond for jit in 0.000418901 sec

Here's a repro code that prints the cache misses (including the jax-level prints)

import weakref
from absl import app
from flax import nnx
from flax.nnx.transforms import transforms as nnx_transforms
import jax

jax.config.update("jax_log_compiles", True)
jax.config.update("jax_explain_cache_misses", True)


def true_fn(x):
  return x + 1.0


def false_fn(x):
  return x - 1.0


original_SimpleCondFn = nnx_transforms.SimpleCondFn


def run_test(name, patch_fn):
  print(f"\n--- Running Test: {name} ---", flush=True)

  patch_fn()

  def test_fn(pred, x):
    # Pass graph_updates=False to force the path that uses SimpleCondFn
    return nnx.cond(pred, true_fn, false_fn, x, graph_updates=False)

  for i in range(3):
    print(f"\nIteration {i}:", flush=True)
    pred = jax.numpy.array(i % 2 == 0)
    res = test_fn(pred, 1.0)
    jax.block_until_ready(res)


# Buggy implementation (original behavior, but with prints)
def apply_buggy_patch():
  # We want to intercept calls to SimpleCondFn to print when they are created
  def buggy_SimpleCondFn_factory(f, graph):
    instance = original_SimpleCondFn(f, graph)
    print(
        f"  [BUGGY] Created new wrapper for {f.__name__} (id={id(f)}):"
        f" wrapper_id={id(instance)}",
        flush=True,
    )
    return instance

  nnx_transforms.SimpleCondFn = buggy_SimpleCondFn_factory


# Patched implementation (using cache)
_repro_cache = weakref.WeakKeyDictionary()


def apply_patched_patch():
  _repro_cache.clear()  # Clear cache for clean run

  def patched_SimpleCondFn_factory(f, graph):
    if f not in _repro_cache:
      _repro_cache[f] = {}
    if graph not in _repro_cache[f]:
      instance = original_SimpleCondFn(f, graph)
      _repro_cache[f][graph] = instance
      print(
          f"  [PATCHED] Cache MISS for {f.__name__} (id={id(f)}). Created"
          f" wrapper_id={id(instance)}",
          flush=True,
      )
    else:
      instance = _repro_cache[f][graph]
      print(
          f"  [PATCHED] Cache HIT for {f.__name__} (id={id(f)}). Reused"
          f" wrapper_id={id(instance)}",
          flush=True,
      )
    return instance

  nnx_transforms.SimpleCondFn = patched_SimpleCondFn_factory


def main(argv):
  if len(argv) > 1:
    raise app.UsageError("Too many command-line arguments.")

  print(f"Global true_fn id: {id(true_fn)}")
  print(f"Global false_fn id: {id(false_fn)}")

  # Run buggy first
  run_test("BUGGY (Original)", apply_buggy_patch)

  # Run patched
  run_test("PATCHED", apply_patched_patch)
  print("\n--- Test Finished ---", flush=True)


if __name__ == "__main__":
  app.run(main)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions