sgl-project--sglang
94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
377 行
12 KiB
Python
可执行文件
377 行
12 KiB
Python
可执行文件
#!/usr/bin/env python3
|
|
"""Analyze srtslurm logs with opencode inside a Modal sandbox.
|
|
|
|
This script accepts either:
|
|
- a local log directory
|
|
- a `.tar.gz` bundle such as `multinode_server_logs.tar.gz`
|
|
|
|
It uploads the logs into an ephemeral Modal sandbox, installs and runs
|
|
opencode with an analysis prompt, and prints the resulting markdown.
|
|
|
|
Example:
|
|
uv run --with modal python scripts/ci/slurm/analyze_logs_with_modal.py \
|
|
--tarball /tmp/multinode_server_logs.tar.gz \
|
|
--job-id 4645
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import logging
|
|
import os
|
|
import re
|
|
import shlex
|
|
import shutil
|
|
import tarfile
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
try:
|
|
import modal
|
|
except ImportError: # pragma: no cover - runtime guard for local usage
|
|
modal = None
|
|
|
|
|
|
logger = logging.getLogger("slurm_log_analysis")
|
|
|
|
SANDBOX_TIMEOUT = 600
|
|
DEFAULT_MODAL_SECRET_NAME = "or"
|
|
DEFAULT_MODEL = "openrouter/minimax/minimax-m2.7"
|
|
DEFAULT_REPOS = [
|
|
"https://github.com/sgl-project/sglang.git",
|
|
]
|
|
PROMPT_PATH = Path(__file__).with_name("log_analysis_prompt.md")
|
|
|
|
# Matches common API key / token patterns (sk-..., ak-..., as-..., key-..., etc.)
|
|
_SECRET_PATTERN = re.compile(
|
|
r"""(?:"""
|
|
r"""(?:sk|ak|as|key|token|secret|bearer)[-_][A-Za-z0-9_\-]{16,}"""
|
|
r"""|"""
|
|
r"""(?:OPENROUTER_API_KEY|MODAL_TOKEN_ID|MODAL_TOKEN_SECRET|ANTHROPIC_API_KEY)"""
|
|
r"""[=:]\s*\S+"""
|
|
r""")""",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
|
|
def sanitize(text: str) -> str:
|
|
"""Redact strings that look like API keys or secrets."""
|
|
if not text:
|
|
return text
|
|
sanitized = _SECRET_PATTERN.sub("[REDACTED]", text)
|
|
# Also redact any env var values we know are secrets
|
|
for var in ("OPENROUTER_API_KEY", "MODAL_TOKEN_ID", "MODAL_TOKEN_SECRET"):
|
|
val = os.environ.get(var)
|
|
if val and len(val) > 8:
|
|
sanitized = sanitized.replace(val, "[REDACTED]")
|
|
return sanitized
|
|
|
|
|
|
def configure_logging(verbose: bool) -> None:
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(levelname)s: %(message)s",
|
|
)
|
|
logger.setLevel(logging.DEBUG if verbose else logging.INFO)
|
|
|
|
|
|
def extract_tarball(tarball: Path, destination: Path) -> None:
|
|
with tarfile.open(tarball, "r:gz") as archive:
|
|
# Python 3.14 changes the default extraction behavior. Use the
|
|
# data filter when available so extraction remains explicit.
|
|
if "data" in tarfile._NAMED_FILTERS: # type: ignore[attr-defined]
|
|
archive.extractall(destination, filter="data")
|
|
else:
|
|
archive.extractall(destination)
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Analyze a srtslurm log bundle with opencode in Modal."
|
|
)
|
|
source = parser.add_mutually_exclusive_group(required=True)
|
|
source.add_argument(
|
|
"--tarball",
|
|
type=Path,
|
|
help="Path to a local multinode_server_logs.tar.gz bundle.",
|
|
)
|
|
source.add_argument(
|
|
"--log-dir",
|
|
type=Path,
|
|
help="Path to an unpacked log directory.",
|
|
)
|
|
parser.add_argument(
|
|
"--job-id",
|
|
default="unknown",
|
|
help="Job identifier used in the report header and logs.",
|
|
)
|
|
parser.add_argument(
|
|
"--model",
|
|
default=DEFAULT_MODEL,
|
|
help="Model selector to pass to opencode run.",
|
|
)
|
|
parser.add_argument(
|
|
"--output",
|
|
type=Path,
|
|
help="Optional path to write the markdown analysis.",
|
|
)
|
|
parser.add_argument(
|
|
"--repo-url",
|
|
action="append",
|
|
dest="repo_urls",
|
|
help=(
|
|
"Optional extra repo URL to clone into the sandbox for context. "
|
|
"Can be specified multiple times."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--timeout-seconds",
|
|
type=int,
|
|
default=SANDBOX_TIMEOUT,
|
|
help="Sandbox lifetime in seconds.",
|
|
)
|
|
parser.add_argument(
|
|
"--modal-secret-name",
|
|
default=DEFAULT_MODAL_SECRET_NAME,
|
|
help="Modal secret name that provides OPENROUTER_API_KEY to the sandbox.",
|
|
)
|
|
parser.add_argument(
|
|
"--verbose",
|
|
action="store_true",
|
|
help="Enable debug logging.",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def build_sandbox_image() -> modal.Image:
|
|
if modal is None:
|
|
raise RuntimeError(
|
|
"The 'modal' package is required. Run this script with "
|
|
"`uv run --with modal python ...` or install modal locally."
|
|
)
|
|
|
|
return (
|
|
modal.Image.debian_slim(python_version="3.12")
|
|
.apt_install("bash", "curl", "git", "gh")
|
|
.run_commands(
|
|
"curl -fsSL https://opencode.ai/install | bash",
|
|
)
|
|
.env(
|
|
{
|
|
"PATH": "/root/.opencode/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
}
|
|
)
|
|
)
|
|
|
|
|
|
def prepare_log_dir(args: argparse.Namespace) -> tuple[Path, Path | None]:
|
|
if args.log_dir:
|
|
if not args.log_dir.is_dir():
|
|
raise FileNotFoundError(f"log directory not found: {args.log_dir}")
|
|
return args.log_dir.resolve(), None
|
|
|
|
assert args.tarball is not None
|
|
if not args.tarball.is_file():
|
|
raise FileNotFoundError(f"tarball not found: {args.tarball}")
|
|
|
|
temp_dir = Path(tempfile.mkdtemp(prefix="sglang_logs_"))
|
|
extract_tarball(args.tarball, temp_dir)
|
|
return temp_dir.resolve(), temp_dir
|
|
|
|
|
|
def build_prompt(job_id: str, repo_urls: list[str]) -> str:
|
|
skill_content = PROMPT_PATH.read_text()
|
|
repo_lines = []
|
|
for repo_url in repo_urls:
|
|
repo_name = repo_url.rsplit("/", 1)[-1].removesuffix(".git")
|
|
repo_lines.append(f"- **{repo_name} repo**: `/workspace/repos/{repo_name}/`")
|
|
repo_section = (
|
|
"\n".join(repo_lines) if repo_lines else "- No extra repos were requested."
|
|
)
|
|
|
|
return f"""{skill_content}
|
|
|
|
---
|
|
|
|
## Your Environment
|
|
|
|
- **Logs**: `/workspace/logs/`
|
|
- **GitHub CLI**: `gh` is installed and authenticated.
|
|
{repo_section}
|
|
|
|
## Job
|
|
|
|
You are analyzing job `{job_id}`. Follow Steps 1–5 in the prompt above.
|
|
|
|
**You MUST write the final markdown report to `/workspace/logs/ai_analysis.md`.**
|
|
This is a hard requirement. Do not just print the report to stdout. Use your
|
|
file-writing tool to create `/workspace/logs/ai_analysis.md` with the full
|
|
analysis. The downstream pipeline reads this file.
|
|
|
|
**You MUST file GitHub issues when the root cause is clear (Category A or B).**
|
|
Do not skip issue filing. The whole point of this system is automated triage.
|
|
"""
|
|
|
|
|
|
def upload_tree(sandbox: modal.Sandbox, log_dir: Path) -> None:
|
|
log_files = [path for path in log_dir.rglob("*") if path.is_file()]
|
|
logger.info("Uploading %d log files into the sandbox", len(log_files))
|
|
for index, log_file in enumerate(log_files, start=1):
|
|
rel_path = log_file.relative_to(log_dir)
|
|
remote_path = str(Path("/workspace/logs") / rel_path)
|
|
sandbox.mkdir(str(Path(remote_path).parent), parents=True)
|
|
sandbox.filesystem.write_bytes(log_file.read_bytes(), remote_path)
|
|
if index % 10 == 0 or index == len(log_files):
|
|
logger.info("Uploaded %d/%d files", index, len(log_files))
|
|
|
|
|
|
def clone_context_repos(sandbox: modal.Sandbox, repo_urls: list[str]) -> None:
|
|
if not repo_urls:
|
|
return
|
|
|
|
sandbox.mkdir("/workspace/repos", parents=True)
|
|
|
|
for repo_url in repo_urls:
|
|
repo_name = repo_url.rsplit("/", 1)[-1].removesuffix(".git")
|
|
logger.info("Cloning %s into the sandbox", repo_name)
|
|
sandbox.exec(
|
|
"git",
|
|
"clone",
|
|
"--depth",
|
|
"100",
|
|
repo_url,
|
|
f"/workspace/repos/{repo_name}",
|
|
).wait()
|
|
|
|
|
|
def read_optional_file(sandbox: modal.Sandbox, path: str) -> str | None:
|
|
try:
|
|
return sandbox.filesystem.read_text(path)
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def run_opencode_analysis(
|
|
*,
|
|
log_dir: Path,
|
|
job_id: str,
|
|
model: str,
|
|
repo_urls: list[str],
|
|
timeout_seconds: int,
|
|
modal_secret_name: str,
|
|
) -> str:
|
|
prompt = build_prompt(job_id, repo_urls)
|
|
app = modal.App.lookup("sglang-log-analyzer", create_if_missing=True)
|
|
sandbox = modal.Sandbox.create(
|
|
app=app,
|
|
image=build_sandbox_image(),
|
|
timeout=timeout_seconds,
|
|
secrets=[modal.Secret.from_name(modal_secret_name)],
|
|
)
|
|
logger.info("Created Modal sandbox %s", sandbox.object_id)
|
|
|
|
try:
|
|
sandbox.mkdir("/workspace/logs", parents=True)
|
|
sandbox.mkdir("/workspace/repos", parents=True)
|
|
|
|
clone_context_repos(sandbox, repo_urls)
|
|
upload_tree(sandbox, log_dir)
|
|
|
|
sandbox.filesystem.write_text(prompt, "/workspace/prompt.txt")
|
|
|
|
runner_script = f"""#!/bin/bash
|
|
set -uo pipefail
|
|
cd /workspace
|
|
if opencode run \\
|
|
--dangerously-skip-permissions \\
|
|
--dir /workspace/logs \\
|
|
-m {shlex.quote(model)} \\
|
|
"$(cat /workspace/prompt.txt)" \\
|
|
< /dev/null \\
|
|
> /workspace/logs/opencode.stdout \\
|
|
2> /workspace/logs/opencode.stderr; then
|
|
echo 0 > /workspace/logs/opencode.exitcode
|
|
else
|
|
echo $? > /workspace/logs/opencode.exitcode
|
|
fi
|
|
ls -la /workspace/logs > /workspace/logs/log_dir_listing.txt
|
|
"""
|
|
sandbox.filesystem.write_text(runner_script, "/workspace/run_opencode.sh")
|
|
sandbox.exec("chmod", "+x", "/workspace/run_opencode.sh").wait()
|
|
|
|
logger.info("Running opencode analysis")
|
|
process = sandbox.exec(
|
|
"bash",
|
|
"/workspace/run_opencode.sh",
|
|
)
|
|
process.wait()
|
|
|
|
stderr = process.stderr.read()
|
|
if stderr:
|
|
logger.warning("runner stderr: %s", sanitize(stderr[:500]))
|
|
|
|
exitcode = read_optional_file(sandbox, "/workspace/logs/opencode.exitcode")
|
|
opencode_stdout = (
|
|
read_optional_file(sandbox, "/workspace/logs/opencode.stdout") or ""
|
|
)
|
|
opencode_stderr = read_optional_file(sandbox, "/workspace/logs/opencode.stderr")
|
|
log_dir_listing = read_optional_file(
|
|
sandbox, "/workspace/logs/log_dir_listing.txt"
|
|
)
|
|
try:
|
|
ai_analysis = read_optional_file(sandbox, "/workspace/logs/ai_analysis.md")
|
|
if ai_analysis and ai_analysis.strip():
|
|
return sanitize(ai_analysis)
|
|
|
|
if opencode_stdout.strip():
|
|
return sanitize(opencode_stdout)
|
|
|
|
raise RuntimeError("opencode completed without producing analysis output")
|
|
except Exception as exc:
|
|
stdout = process.stdout.read()
|
|
if stdout:
|
|
return sanitize(stdout)
|
|
details = [
|
|
f"opencode analysis did not produce a usable report: {exc}",
|
|
f"exitcode={exitcode!r}",
|
|
f"stdout_preview={sanitize(opencode_stdout[:500])!r}",
|
|
f"stderr_preview={sanitize((opencode_stderr or '')[:500])!r}",
|
|
f"log_dir_listing={sanitize((log_dir_listing or '')[:500])!r}",
|
|
]
|
|
raise RuntimeError(" ".join(details)) from exc
|
|
finally:
|
|
sandbox.terminate()
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
configure_logging(args.verbose)
|
|
|
|
repo_urls = list(DEFAULT_REPOS)
|
|
if args.repo_urls:
|
|
repo_urls.extend(args.repo_urls)
|
|
|
|
log_dir, cleanup_dir = prepare_log_dir(args)
|
|
try:
|
|
analysis = run_opencode_analysis(
|
|
log_dir=log_dir,
|
|
job_id=args.job_id,
|
|
model=args.model,
|
|
repo_urls=repo_urls,
|
|
timeout_seconds=args.timeout_seconds,
|
|
modal_secret_name=args.modal_secret_name,
|
|
)
|
|
finally:
|
|
if cleanup_dir is not None:
|
|
shutil.rmtree(cleanup_dir, ignore_errors=True)
|
|
|
|
print(analysis)
|
|
if args.output:
|
|
args.output.write_text(analysis)
|
|
logger.info("Wrote analysis to %s", args.output)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|