Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions mstar/engine/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import torch

torch._dynamo.config.recompile_limit = 84
torch._dynamo.config.allow_unspec_int_on_nn_module = True
torch._dynamo.config.specialize_int = False
torch.set_float32_matmul_precision('high')
# torch._dynamo ConfigModule stores user overrides in a ContextVar (thread-local
# as of torch 2.13+). Import-time assignments only affect the importing thread;
# the dedicated GPU executor thread that actually compiles never sees them.
# Call apply_dynamo_config() on any thread that may trigger dynamo tracing.
RECOMPILE_LIMIT = 84

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RECOMPILE_LIMIT should be able to be overridden as an environment variable, with reasonable minimum and maximum limits set in the code (and then documented in docs/environment_variables.rst)



def apply_dynamo_config() -> None:
"""Apply dynamo settings to the *calling* thread.

torch's ConfigModule keeps user overrides in a ContextVar, so these are
thread-local — any thread that may trigger a compile must call this.
"""
torch._dynamo.config.recompile_limit = RECOMPILE_LIMIT
torch._dynamo.config.allow_unspec_int_on_nn_module = True
torch._dynamo.config.specialize_int = False


apply_dynamo_config()
torch.set_float32_matmul_precision("high")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although torch.set_float32_matmul_precision("high") doesn't need to be set per-thread, I still think it might be cleaner (and more future-proof) to group it in with the apply_dynamo_config function (possibly giving the function a different name if that makes sense to do).

9 changes: 9 additions & 0 deletions mstar/engine/cuda_graph_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,11 +609,14 @@ def _capture_slots(
# graph (finished in the submodule ``postprocess``).
forward = getattr(submodule, config.capture_forward_method)
if config.compile:
from mstar.engine import RECOMPILE_LIMIT

forward = torch.compile(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads up: a bare RECOMPILE_LIMIT constant fixes today's config set but stays fragile. Dynamo keys the recompile counter on the underlying code object, not the torch.compile wrapper, so in the cuda graph setting, every slot, every batch size, every config sharing a capture_forward_method, plus the eager path that compiles the same forward_batched, all accumulate onto one counter.

With dynamic=False (which we use to get maximum performance out of the cuda graph path), each static shape is one specialization, so the entries on that counter = sum(batch_sizes × token_buckets) over those configs + eager runtime shapes. A fixed constant has no relationship to that sum, so adding a batch size later can silently push it over, resulting eager fallback mid-capture. Right now, the RECOMPILE_LIMIT is high enough that we don't see that in practice

Suggest deriving it instead: recompile_limit = min(max(FLOOR, capture_buckets_for_this_code_object + EAGER_HEADROOM), CEIL) (with FLOOR, CEIL, and EAGER_HEADROOM being the knobs rather than RECOMPILE_LIMIT. I think the easiest way to get "capture_buckets_for_this_code_object" is to have a function on the base submodule that computes it based on the cuda graph configs.

forward,
mode="max-autotune-no-cudagraphs",
fullgraph=False,
dynamic=False,
recompile_limit=RECOMPILE_LIMIT,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I realize that my suggested fix in #167 of setting recompile_limit on torch.compile only works for torch 2.13; before then, the only lever for setting the recompile limit is the global one. I think in that case, since we only pin torch>=2.9.1 in pyproject.toml, we should rely on the global lever torch._dynamo.config.recompile_limit only for now (instead of, e.g., including the kwarg contingent on the right value of torch; we can change the default behavior down the road after pinning a newer torch).

With respect to my comment https://github.com/mstar-project/mstar/pull/178/changes#r3612126831 about computing the recompile limit based on the cuda graph configurations, I think the best way to proceed is to put apply_dynamo_config onto EngineManager, with the following additional changes needed to make it work:

  1. Have EngineManager now hold the node_submodules: dict[str, NodeSubmodule] mapping computed when building the engine manager. node_submodules is already computed as a local in build() today, so this is just promoting an existing local to a field, not new plumbing.
  2. Add a function on the engine manager to compute the recompile limit (as the maximum recompile limit across submodules). EngineManager.apply_dynamo_config will call this function.
  3. Call self.engine_manager.apply_dynamo_config in worker.pyafter EngineManager.build in worker.py, and also use that function as the initializer for the thread pool executor. Note that both call sites are needed: the former because the warmup (cuda graph capture) occurs on the main thread, the latter for request execution in the async worker.

)
spec.engine_inputs.sampler.applied_penalty_in_graph = False

Expand Down Expand Up @@ -2184,11 +2187,14 @@ def _capture_one(

fwd = submodule.forward_batched
if config.compile:
from mstar.engine import RECOMPILE_LIMIT

fwd = torch.compile(
fwd,
mode="max-autotune-no-cudagraphs",
fullgraph=False,
dynamic=False,
recompile_limit=RECOMPILE_LIMIT,
)

# Warmup and capture on the shared side stream (see warmup_and_capture
Expand Down Expand Up @@ -2509,11 +2515,14 @@ def _capture_one(self, shape: PiecewiseCaptureShape) -> None:

fn = self.config.capture_fn
if self.config.compile:
from mstar.engine import RECOMPILE_LIMIT

fn = torch.compile(
fn,
mode="max-autotune-no-cudagraphs",
fullgraph=False,
dynamic=False,
recompile_limit=RECOMPILE_LIMIT,
)

def run_fn():
Expand Down
4 changes: 4 additions & 0 deletions mstar/engine/kv_cache_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,15 +332,19 @@ def _compile_submodules(self) -> None:
continue

try:
from mstar.engine import RECOMPILE_LIMIT

submodule.forward = torch.compile(
submodule.forward,
fullgraph=False,
dynamic=None,
recompile_limit=RECOMPILE_LIMIT,
)
submodule.forward_batched = torch.compile(
submodule.forward_batched,
fullgraph=False,
dynamic=None,
recompile_limit=RECOMPILE_LIMIT,
)
logger.info("KVCacheEngine: torch.compile applied to %s language_model", node_name)
except Exception:
Expand Down
3 changes: 3 additions & 0 deletions mstar/engine/stateless_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,9 +545,12 @@ def _apply_torch_compile(self, node_name: str, submodule: NodeSubmodule) -> None
return
try:
if hasattr(submodule, "forward"):
from mstar.engine import RECOMPILE_LIMIT

submodule.forward = torch.compile(
submodule.forward,
fullgraph=False,
recompile_limit=RECOMPILE_LIMIT,
)
logger.info(
"StatelessEngine[%s]: torch.compile applied to %s.forward",
Expand Down
7 changes: 6 additions & 1 deletion mstar/worker/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from mstar.conductor.request_info import CurrentForwardPassInfo
from mstar.distributed.base import ShardingConfig
from mstar.distributed.communication import WorkerParallelGroups
from mstar.engine import apply_dynamo_config
from mstar.engine.base import EngineType, NodeBatch, NodeOutput
from mstar.engine.kv_store import KVCacheConfig, StoreWritePolicy, TransferEngineInfo
from mstar.graph.base import GraphEdge, GraphNode
Expand Down Expand Up @@ -1974,8 +1975,12 @@ def run(self) -> None:
# the main loop can overlap queue/tensor polling and post-processing
# with GPU execution. Run the engine unconditionally on a dedicated
# 1-worker GPU thread.
# initializer re-applies dynamo config on the GPU thread — ConfigModule
# overrides are thread-local (ContextVar) as of torch 2.13+ (#167).
gpu_executor = ThreadPoolExecutor(
max_workers=1, thread_name_prefix=f"mstar-gpu-{self.worker_id}"
max_workers=1,
thread_name_prefix=f"mstar-gpu-{self.worker_id}",
initializer=apply_dynamo_config,
)
logger.info(
"Worker %s: engine runs on dedicated GPU thread",
Expand Down
59 changes: 59 additions & 0 deletions test/modular/test_dynamo_thread_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Regression: dynamo config must apply on worker threads (#167).

torch._dynamo ConfigModule stores user overrides in a ContextVar (thread-local
as of torch 2.13+). Import-time assignments on the main thread do not propagate
to the dedicated GPU executor thread. apply_dynamo_config() must be called on
any thread that may trigger dynamo tracing.
"""

from concurrent.futures import ThreadPoolExecutor

import torch

from mstar.engine import RECOMPILE_LIMIT, apply_dynamo_config

_KEYS = ("recompile_limit", "allow_unspec_int_on_nn_module", "specialize_int")


def _read_dynamo_flags() -> dict:
return {k: getattr(torch._dynamo.config, k) for k in _KEYS}


def test_apply_dynamo_config_on_worker_thread():
"""GPU-thread initializer path: flags match RECOMPILE_LIMIT / expected defaults."""
# Main thread already applied at import; re-apply for a clean baseline.
apply_dynamo_config()
main = _read_dynamo_flags()
assert main["recompile_limit"] == RECOMPILE_LIMIT
assert main["allow_unspec_int_on_nn_module"] is True
assert main["specialize_int"] is False

with ThreadPoolExecutor(max_workers=1, initializer=apply_dynamo_config) as ex:
worker = ex.submit(_read_dynamo_flags).result()

assert worker["recompile_limit"] == RECOMPILE_LIMIT
assert worker["allow_unspec_int_on_nn_module"] is True
assert worker["specialize_int"] is False


def test_import_time_flags_do_not_leak_to_fresh_thread_without_initializer():
"""Document the torch 2.13+ ContextVar pitfall the fix addresses.

On builds where ConfigModule is thread-local, a fresh thread without
apply_dynamo_config() does not see main-thread overrides. Skip when the
installed torch still shares config across threads (pre-2.13 behavior).
"""
apply_dynamo_config()
main = _read_dynamo_flags()

with ThreadPoolExecutor(max_workers=1) as ex:
worker = ex.submit(_read_dynamo_flags).result()

if worker == main:
# Older torch: config is process-global — nothing to regress against.
return

# Thread-local: worker should have fallen back away from our overrides.
assert worker["recompile_limit"] != RECOMPILE_LIMIT or worker[
"allow_unspec_int_on_nn_module"
] is not True