Repository navigation
[CI] Add E2E CI for Tinker API with SkyRLTrainBackend
#1616
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
6c4e53b
216262a
351c0ef
89bf89a
9015836
d6963e9
e7b42f4
c9db628
cea1faa
84f8fc4
7f487e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| name: SkyRL-GPU-E2E-CI-Tinker | ||
|
|
||
| on: | ||
| schedule: | ||
| - cron: '17 8 * * *' # Every day at 08:17 UTC, offset from the other nightly E2Es | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| checks: write # for status checks to appear | ||
| contents: read | ||
|
|
||
| jobs: | ||
|
|
||
| skyrl_gpu_e2e_test_tinker: | ||
| runs-on: ubuntu-latest | ||
| defaults: | ||
| run: | ||
| shell: bash | ||
| working-directory: . | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Set up Python | ||
| # This is the version of the action for setting up Python, not the Python version. | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| # Semantic version range syntax or exact version of a Python version | ||
| python-version: '3.12' | ||
| cache: 'pip' | ||
| - name: Install the latest version of uv | ||
| uses: astral-sh/setup-uv@v6 | ||
| with: | ||
| activate-environment: true | ||
| - name: Install basic dependencies | ||
| run: uv pip install anyscale==0.24.79 typer==0.9.0 | ||
| - name: Install envsubst | ||
| run: sudo apt-get update && sudo apt-get install -y gettext-base | ||
| - name: Tinker rl_loop convergence test | ||
| env: | ||
| ANYSCALE_CLI_TOKEN: ${{ secrets.ANYSCALE_CLI_TOKEN }} | ||
| ANYSCALE_HOST: https://console.anyscale.com | ||
| WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }} | ||
| run: | | ||
| envsubst < ci/anyscale_gpu_e2e_test_tinker.yaml > ci/anyscale_gpu_e2e_test_tinker_envsubst.yaml | ||
| anyscale job submit -f ci/anyscale_gpu_e2e_test_tinker_envsubst.yaml --timeout 12000 | ||
| anyscale job wait --cloud sky-anyscale-aws-us-east-1 --name skyrl-train-gpu-e2e-test-tinker --timeout 12000 | ||
| rm -f ci/anyscale_gpu_e2e_test_tinker_envsubst.yaml |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| name: skyrl-train-gpu-e2e-test-tinker | ||
| entrypoint: bash ci/gpu_e2e_test_run_tinker.sh | ||
| image_uri: novaskyai/skyrl-train-ray-2.51.1-py3.12-cu12.8 # (Optional) Exclusive with `containerfile`. | ||
| cloud: sky-anyscale-aws-us-east-1 | ||
| ray_version: "2.51.1" | ||
| compute_config: l4_ci | ||
| working_dir: . # (Optional) Use current working directory "." as the working_dir. Can be any local path or remote .zip file in cloud storage. | ||
| env_vars: | ||
| WANDB_API_KEY: $WANDB_API_KEY | ||
| max_retries: 1 # (Optional) Maximum number of times the job will be retried before being marked failed. Defaults to `1`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| bash tests/train/gpu_e2e_test/gsm8k_tinker.sh |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ | |
| init_app_state, | ||
| ) | ||
| from vllm.inputs import TokensPrompt | ||
| from vllm.lora.request import LoRARequest | ||
| from vllm.sampling_params import SamplingParams as VLLMSamplingParams | ||
| from vllm.usage.usage_lib import UsageContext | ||
| from vllm.utils import random_uuid | ||
|
|
@@ -360,6 +361,47 @@ async def _reset_prefix_cache(request: Request): | |
| await engine.reset_prefix_cache() | ||
| return {"status": "ok"} | ||
|
|
||
| @app.post("/skyrl/v1/load_lora_adapter") | ||
| async def _skyrl_load_lora_adapter(request: Request): | ||
| """Load a LoRA adapter from disk, replacing any existing adapter | ||
| under the same name in place. Used by RemoteInferenceClient.update_lora_from_disk. | ||
|
|
||
| TODO(aaron): remove this endpoint and route update_lora_from_disk back | ||
| through /v1/load_lora_adapter once the upstream fix in | ||
| https://github.com/vllm-project/vllm/pull/41482 lands in a vLLM release we depend on. | ||
| """ | ||
| body = await request.json() | ||
| lora_name = body.get("lora_name") | ||
| lora_path = body.get("lora_path") | ||
| if not lora_name or not lora_path: | ||
| raise HTTPException( | ||
| status_code=400, | ||
| detail="Both 'lora_name' and 'lora_path' must be provided.", | ||
| ) | ||
|
|
||
| models = request.app.state.openai_serving_models | ||
| async with models.lora_resolver_lock[lora_name]: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The if not hasattr(models, "lora_requests") or models.lora_requests is None:
raise HTTPException(
status_code=400,
detail="LoRA is not enabled on this server.",
)
async with models.lora_resolver_lock[lora_name]: |
||
| lora_int_id = ( | ||
| models.lora_requests[lora_name].lora_int_id | ||
| if lora_name in models.lora_requests | ||
| else models.lora_id_counter.inc(1) | ||
| ) | ||
| lora_request = LoRARequest( | ||
| lora_name=lora_name, | ||
| lora_int_id=lora_int_id, | ||
| lora_path=lora_path, | ||
| load_inplace=True, | ||
| ) | ||
| await models.engine_client.add_lora(lora_request) | ||
| lora_request.load_inplace = False | ||
| models.lora_requests[lora_name] = lora_request | ||
|
|
||
| return { | ||
| "status": "ok", | ||
| "lora_name": lora_name, | ||
| "lora_int_id": lora_int_id, | ||
| } | ||
|
|
||
| # NOTE (sumanthrh): We use a custom generate endpoint /skyrl/v1/generate because the native | ||
| # endpoint /inference/v1/generate does not support returning routed expert IDs. | ||
| # TODO (sumanthrh): Migrate back to /inference/v1/generate once this is fixed on the vllm side | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| RUN_NAME="run_$(date +%Y%m%d%H)" | ||
| PROJECT_NAME="gsm8k_tinker_ci" | ||
| SCRIPT_DIR=$(dirname $(realpath $0)) | ||
| SKYRL_REPO_ROOT=$(realpath "$SCRIPT_DIR/../../..") | ||
| LOG_DIR="$HOME/tinker_logs/$RUN_NAME" | ||
| mkdir -p "$LOG_DIR" | ||
|
|
||
| # TODO: tighten thresholds after 3-5 nightly runs (5% allowance from min observed), | ||
| # matching the convention in gsm8k_colocate.sh. | ||
| REWARD_MIN_VALUE=0.0 | ||
|
|
||
| BACKEND_CONFIG='{"trainer.placement.colocate_all": true, "trainer.placement.policy_num_gpus_per_node": 4, "trainer.micro_forward_batch_size_per_gpu": 8, "trainer.micro_train_batch_size_per_gpu": 8, "generator.inference_engine.num_engines": 4, "generator.inference_engine.tensor_parallel_size": 1, "generator.inference_engine.backend": "vllm", "generator.inference_engine.run_engines_locally": true, "generator.inference_engine.weight_sync_backend": "nccl", "generator.inference_engine.async_engine": true, "generator.inference_engine.gpu_memory_utilization": 0.8, "generator.batched": true}' | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer if the default tinker test is non-colocated, 2 GPUs for trainer and 2 for inference.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually can you just add a separate non-colocated E2E test?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added |
||
|
|
||
| # Start tinker server in its own process group so we can clean up the engine subprocess too. | ||
| setsid uv run --extra tinker --extra fsdp -m skyrl.tinker.api \ | ||
| --base-model "Qwen/Qwen3-0.6B" --backend fsdp --port 8000 \ | ||
| --backend-config "$BACKEND_CONFIG" >"$LOG_DIR/server.log" 2>&1 & | ||
| SERVER_PID=$! | ||
| trap 'kill -TERM -- -$SERVER_PID 2>/dev/null || true; sleep 5; kill -KILL -- -$SERVER_PID 2>/dev/null || true' EXIT | ||
|
|
||
| deadline=$(( $(date +%s) + 1800 )) | ||
| until curl -sSf http://localhost:8000/docs >/dev/null 2>&1; do | ||
| if (( $(date +%s) > deadline )); then | ||
| echo "Tinker server did not become ready within 30 minutes" >&2 | ||
| tail -n 200 "$LOG_DIR/server.log" >&2 || true | ||
| exit 1 | ||
| fi | ||
| if ! kill -0 $SERVER_PID 2>/dev/null; then | ||
| echo "Tinker server exited early" >&2 | ||
| tail -n 200 "$LOG_DIR/server.log" >&2 || true | ||
| exit 1 | ||
| fi | ||
| sleep 5 | ||
| done | ||
|
|
||
| COOKBOOK_DIR="$HOME/tinker-cookbook" | ||
| [ -d "$COOKBOOK_DIR" ] || git clone --depth 1 https://github.com/thinking-machines-lab/tinker-cookbook.git "$COOKBOOK_DIR" | ||
|
|
||
| # math_rl.train builds on tinker_cookbook/rl/train.py and exposes wandb_project / | ||
| # wandb_name natively, so we get the same wandb-driven flow as the other E2E | ||
| # nightlies (no client-side metrics publisher needed). | ||
| cd "$COOKBOOK_DIR" | ||
| TINKER_API_KEY=tml-dummy uv run --extra math-rl --extra wandb --with tinker --with datasets --with torch \ | ||
| python -m tinker_cookbook.recipes.math_rl.train \ | ||
| base_url=http://localhost:8000 \ | ||
| model_name="Qwen/Qwen3-0.6B" \ | ||
| env=gsm8k \ | ||
| log_path="$LOG_DIR" \ | ||
| groups_per_batch=512 \ | ||
| group_size=4 \ | ||
| max_tokens=512 \ | ||
| max_steps=14 \ | ||
| eval_every=10000 \ | ||
| save_every=10000 \ | ||
| wandb_project="$PROJECT_NAME" \ | ||
| wandb_name="$RUN_NAME" \ | ||
| behavior_if_log_dir_exists=delete | ||
|
|
||
| cd "$SKYRL_REPO_ROOT" | ||
| uv run --isolated --extra fsdp "$SCRIPT_DIR/get_summary.py" \ | ||
| --run_name "$RUN_NAME" --project_name "$PROJECT_NAME" \ | ||
| --asserts "env/all/reward/total >= $REWARD_MIN_VALUE" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The return value of
_load_on_serveris inconsistent with other control plane methods in this class (e.g.,_call_serverat line 857), which return a dictionary containing both the HTTP status and the response body. This inconsistency may cause issues for callers expecting the standard{url: {"status": ..., "body": ...}}format.