Skip to content

[Tunix] Support DeepSWE evaluation pipeline with MaxText and vLLM on TPU - #2094

Open
susanbao wants to merge 2 commits into
mainfrom
sanbao/eval_pr
Open

[Tunix] Support DeepSWE evaluation pipeline with MaxText and vLLM on TPU#2094
susanbao wants to merge 2 commits into
mainfrom
sanbao/eval_pr

Conversation

@susanbao

@susanbao susanbao commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Resolves #

It's a good idea to open an issue first for discussion.

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:

  • DeepSWE Evaluation Pipeline (examples/deepswe/eval_deepswe.py):
    • Adds CLI argument parsing (--model_version, --model_source, --model_absolute_path, --mesh_fsdp, --mesh_tp, --scan_layers, etc.) with fallback to environment variables.
    • Supports loading MaxText models (e.g. Qwen3.5-35B-A3B) directly into VllmSampler or via AutoModel.
    • Integrates 3D MRoPE position normalization patch for Qwen3-Omni-Moe thinker models in MaxText.
    • Dynamically configures Kubernetes nodeSelector via patch_kubernetes_runtime for GKE nodepools.
    • Implements Pass@k metric computation (Pass@1, Pass@4, Pass@5) using an unbiased combinatorial estimator across instance groups.
    • Adds direct GCS upload support for evaluation results using google-cloud-storage with gcloud storage cp fallback.
    • Adds garbage collection (gc.collect()) after model calls for memory management.
  • AutoModel (tunix/models/automodel.py):
    • Whitelists checkpoint_storage_concurrent_gb in AutoModel.from_pretrained kwargs for Orbax checkpoint restore configuration.
  • Trajectory Engine (tunix/rl/agentic/trajectory/trajectory_collect_engine.py):
    • Safely extracts prompt tokens from either left_padded_prompt_tokens or padded_prompt_tokens on SamplerOutput to prevent prompt desync.

Reference

Colab Notebook

Checklist

  • I have added all the necessary unit tests for my change.
  • I have verified that my change does not break existing code and all unit tests pass.
  • I have added all appropriate doc-strings/documentation.
  • My PR is based on the latest changes of the main branch (if unsure, rebase the code).
  • I have signed the Contributor License Agreement.
  • I have followed Contribution Guidelines.

Note: Standard CPU unit tests, package builds, and documentation checks will run automatically on pull requests. Once the PR is approved and ready for submission, maintainers will add the ready-to-submit label to trigger full TPU testing.

- 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 RissyRan left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the change! A few comments.

Comment thread examples/deepswe/eval_deepswe.py Outdated

def _patched_qwen_mrope_call(self, inputs, position, *args, **kwargs):
if position is not None:
# Case 1: vLLM (3, N, 1) -> (N, 1, 3)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

How do we test them?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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).

Comment thread examples/deepswe/eval_deepswe.py Outdated
…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):

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.

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():

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.

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?

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.

TP_SIZE,
)

if MODEL_VERSION == "Qwen/Qwen3-4B-Instruct-2507":

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.

why remove these checks?

MODEL_PATH, model_config, mesh, dtype=jnp.float32
total_mesh_devices,
)
sft_utils.show_hbm_usage()

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.

ditto, why remove these logging and info?


logger.info("Creating sampler with engine=%s ...", ROLLOUT_ENGINE)

if ROLLOUT_ENGINE == "vanilla":

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.

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(

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.

why use getattr? won't rollout_output always have left_padded_prompt_tokens?

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.

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:

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.

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()

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.

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,

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.

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.

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.

why are these doc string removed?

import time

# Path Setup before JAX
workdir = os.getcwd()

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.

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:

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.

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":

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.

please put this into a maxtext_model_helper file

cache_dir=DATASET_CACHE,
num_proc=32,
)
if DATASET_NUM_PROC > 1:

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.

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"),

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.

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():

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.

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):

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.

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.

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.

5 participants