[Tunix] Support DeepSWE evaluation pipeline with MaxText and vLLM on TPU - #2094
[Tunix] Support DeepSWE evaluation pipeline with MaxText and vLLM on TPU#2094susanbao wants to merge 2 commits into
Conversation
- examples/deepswe/eval_deepswe.py: - Add CLI arguments for model, dataset, and runtime configuration - Add MaxText checkpoint loading support in vLLM sampler and AutoModel - Add patch for 3D MRoPE position normalization with Qwen thinker models - Configure Kubernetes nodeSelector for GKE nodepools - Compute pass@k metrics (pass@1, pass@4, pass@5) with unbiased estimator - Support direct GCS upload for evaluation results - tunix/models/automodel.py: - Whitelist checkpoint_storage_concurrent_gb in AutoModel kwargs - tunix/rl/agentic/trajectory/trajectory_collect_engine.py: - Safely extract prompt tokens from either left_padded_prompt_tokens or padded_prompt_tokens
RissyRan
left a comment
There was a problem hiding this comment.
Thanks for the change! A few comments.
|
|
||
| def _patched_qwen_mrope_call(self, inputs, position, *args, **kwargs): | ||
| if position is not None: | ||
| # Case 1: vLLM (3, N, 1) -> (N, 1, 3) |
There was a problem hiding this comment.
Could you help point the source for cases to be handled for user readability? i.e. why those cases exist
# Case 1: vLLM (3, N, 1) -> (N, 1, 3)
# Case 2: vLLM (3, N) -> (N, 1, 3)
# Case 3: (N,) -> (N, 1, 3)
# Case 4: (B, S) -> (B, S, 3)
# Case 5: (B, S, 1) -> (B, S, 3)
I didn't see the else branch, is this expected?
There was a problem hiding this comment.
Thanks for pointing this out! I will extract this logic into a dedicated helper function with clear docstrings explaining each case:
- Why Cases 1 & 2 exist (vLLM MRoPE format): For Qwen thinker/MRoPE models, vLLM provides input_positions with the 3 MRoPE coordinates (temporal, height, width) as the leading dimension: (3, num_tokens) or (3, num_tokens, 1). MaxText's inputs tensor has shape (num_tokens, 1, num_heads, head_dim). If not transposed, computing rotary embeddings yields (3, num_tokens, 1, head_dim), which causes JAX broadcasting failure (N, 1, 16, 64) * (3, N, 1, 64). Transposing to (N, 1, 3) aligns axis 0 to the token sequence dimension.
- Why Cases 3–5 exist (Standard 1D/2D text positions): When text positions come in as (N,), (B, S), or (B, S, 1), MaxText asserts position.shape[-1] == 3. These cases duplicate 1D text positions across the 3 MRoPE coordinate axes.
- Why there is no else branch: If position is already (..., 3) (e.g. (B, S, 3) or (N, 1, 3)), it is already in MaxText's native format and passes through to the original call untouched. I will add an explicit comment and fallback log to make this behavior obvious.
End-to-End Verification: In our cluster tests on TPU v5p-32 running Qwen3.5-35B-A3B and Qwen3-32B, this patch was exercised live across SWE-Bench tasks. Without it, vLLM crashes on the very first forward step with ValueError: Position IDs must be 2D (batch, seq) or 3D (batch, seq, 3) or broadcasting mismatch (8192, 1, 16, 64) vs (3, 8192, 1, 64).
…in eval_deepswe.py - Add str2bool helper for robust boolean flag parsing - Add --dataset_num_proc with automatic fallback to single-process on failure - Add --allow_split_physical_axes (defaults to False to avoid perf degradation) - Add --checkpoint_storage_concurrent_gb (defaults to 32) - Add --weight_dtype, --prefuse_moe_weights, --maxtext_attention, --enable_continue_decode
|
|
||
| _orig_qwen_mrope_call = Qwen3OmniMoeThinkerTextRotaryEmbedding.__call__ | ||
|
|
||
| def _patched_qwen_mrope_call(self, inputs, position, *args, **kwargs): |
There was a problem hiding this comment.
can this be placed in a helper function, ideally not in this file? maybe maxtex_utils.py? it degrades the readability
| logger.info("Kubernetes connection verified.") | ||
|
|
||
| # ========================== Model ========================== | ||
| def patch_kubernetes_runtime(): |
There was a problem hiding this comment.
is this patch_kubernetes_runtime() a change that would be needed both in eval and main deepswe? should we move this a to helper outside eval? maybe r2e_gym_helper.py?
There was a problem hiding this comment.
+1. Nitin had similar stuff here https://github.com/google/tunix/pull/2051/changes#diff-1a6299c4c9d1a03ec4a737371fb4cd43e6280c5a797d324ab5ebeb9df92b97ea
can you reuse?
| TP_SIZE, | ||
| ) | ||
|
|
||
| if MODEL_VERSION == "Qwen/Qwen3-4B-Instruct-2507": |
There was a problem hiding this comment.
why remove these checks?
| MODEL_PATH, model_config, mesh, dtype=jnp.float32 | ||
| total_mesh_devices, | ||
| ) | ||
| sft_utils.show_hbm_usage() |
There was a problem hiding this comment.
ditto, why remove these logging and info?
|
|
||
| logger.info("Creating sampler with engine=%s ...", ROLLOUT_ENGINE) | ||
|
|
||
| if ROLLOUT_ENGINE == "vanilla": |
There was a problem hiding this comment.
why remove vanilla? we use this in our team
| self.agent.trajectory.prompt_tokens = ( # pyrefly: ignore[missing-attribute] | ||
| rollout_output.left_padded_prompt_tokens[0] | ||
| ) | ||
| prompt_tokens = getattr( |
There was a problem hiding this comment.
why use getattr? won't rollout_output always have left_padded_prompt_tokens?
There was a problem hiding this comment.
please make sure you add tests for all changes added,, especially to tunix/ core directories.
| ): | ||
| position = jnp.transpose(position.squeeze(-1), (1, 0))[:, None, :] | ||
| # Case 2: vLLM (3, N) -> (N, 1, 3) | ||
| elif position.ndim == 2 and position.shape[0] == 3: |
There was a problem hiding this comment.
The check elif position.ndim == 2 and position.shape[0] == 3: assumes that any 2D position tensor with leading dimension 3 is a vLLM 3D coordinate tuple (3, N). If a standard 2D batch of shape (batch_size, seq_len) is evaluated with batch_size = 3, this branch incorrectly triggers, no?
| echo=False, | ||
| eos_tokens=qwen_eos_tokens, | ||
| ) | ||
| gc.collect() |
There was a problem hiding this comment.
calling gc collect synchronously inside model_call on every single generation step seems like a poor performance choice
| mesh=mesh, | ||
| hbm_utilization=VLLM_HBM_UTILIZATION, | ||
| init_with_random_weights=VLLM_INIT_RANDOM_WEIGHTS, | ||
| init_with_random_weights=False, |
There was a problem hiding this comment.
why false and not VLLM_INIT_RANDOM_WEIGHTS
| #!/usr/bin/env python | ||
| """DeepSWE evaluation with deepscaler-style task-level parallelism. | ||
|
|
||
| This script intentionally does not modify the existing eval entrypoint. |
There was a problem hiding this comment.
why are these doc string removed?
| import time | ||
|
|
||
| # Path Setup before JAX | ||
| workdir = os.getcwd() |
There was a problem hiding this comment.
why do we need this? if you have the venv with dependencies installed, all these should be available no?
| from guarded_swe_env import GuardedSWEEnv | ||
| from swe_agent import SWEAgent | ||
| from swe_env import SWEEnv | ||
| except ImportError: |
There was a problem hiding this comment.
in what case the import won't be available?
| os.environ["TOKENIZERS_PARALLELISM"] = "true" | ||
|
|
||
| # Register MaxText vLLM adapter if using a MaxText model | ||
| if MODEL_SOURCE == "maxtext": |
There was a problem hiding this comment.
please put this into a maxtext_model_helper file
| cache_dir=DATASET_CACHE, | ||
| num_proc=32, | ||
| ) | ||
| if DATASET_NUM_PROC > 1: |
There was a problem hiding this comment.
why do we need this knob? do you see data loading being slow?
| parser_cli.add_argument( | ||
| "--node_selector_val", | ||
| type=str, | ||
| default=os.getenv("NODE_SELECTOR_VAL", "cpu-np"), |
There was a problem hiding this comment.
why do we want to have two ways to config the CLI, one with py args one with env var, and default one to another? can we just reconcile to one mechanism?
| logger.info("Kubernetes connection verified.") | ||
|
|
||
| # ========================== Model ========================== | ||
| def patch_kubernetes_runtime(): |
There was a problem hiding this comment.
+1. Nitin had similar stuff here https://github.com/google/tunix/pull/2051/changes#diff-1a6299c4c9d1a03ec4a737371fb4cd43e6280c5a797d324ab5ebeb9df92b97ea
can you reuse?
| TP_SIZE = 8 | ||
| mesh_devices = np.array(devices[:TP_SIZE]).reshape(1, TP_SIZE) | ||
| total_mesh_devices = MESH_FSDP * MESH_TP | ||
| if total_mesh_devices > len(devices): |
There was a problem hiding this comment.
a lot of these functions (similar for tokenizer) can just go to a util file. We should keep the recipe script as lean as possible.
Resolves #
Description
This PR enhances the DeepSWE evaluation pipeline (
examples/deepswe/eval_deepswe.py) to support both HuggingFace and MaxText models using vLLM on TPU clusters:examples/deepswe/eval_deepswe.py):--model_version,--model_source,--model_absolute_path,--mesh_fsdp,--mesh_tp,--scan_layers, etc.) with fallback to environment variables.VllmSampleror viaAutoModel.nodeSelectorviapatch_kubernetes_runtimefor GKE nodepools.Pass@1,Pass@4,Pass@5) using an unbiased combinatorial estimator across instance groups.google-cloud-storagewithgcloud storage cpfallback.gc.collect()) after model calls for memory management.tunix/models/automodel.py):checkpoint_storage_concurrent_gbinAutoModel.from_pretrainedkwargs for Orbax checkpoint restore configuration.tunix/rl/agentic/trajectory/trajectory_collect_engine.py):left_padded_prompt_tokensorpadded_prompt_tokensonSamplerOutputto prevent prompt desync.Reference
Colab Notebook
Checklist