项目文件夹

文件
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

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