Skip to content

Commit 79d5c95

Browse files
authored
test: unify cross-backend profiler contract (#154)
* fix: serialize PPU FlagTree compilation FlagTree's PPU driver queries torch.cuda.current_device while selecting compiler hints. Inductor workers may be forked after the parent initializes the PPU CUDA context, causing CUDA reinitialization failures in backward and FP16 compilation.\n\nDefault PPU FlagTree to one compile thread through a scoped Inductor config patch while preserving explicit user settings and all other compiler backends. Document the workaround and cover its scope with regression tests.\n\nTested: ruff check .\nTested: ruff format --check .\nTested: pytest tests/unit/ -q\nTested: pytest tests/integration/test_compile.py -v --tb=short\nTested: PPU FlagTree 0.6.0 full compile suite - 18 passed\n\nRelated: flagos-ai/FlagTree#1031\n\nCo-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test: unify cross-backend profiler contract Run one public torch.profiler contract across supported backends and express tracer differences through capability marks. Replace duplicated hardware modules with shared fixtures and update CI manifests.\n\nTested: pytest tests/integration/test_profiler_contract.py -q --tb=short\nTested: pytest tests/unit/test_profiler_privateuse1.py -q --tb=short\nTested: ruff check and ruff format --check\n\nCo-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: defer profiler support torch import The integration conftest loads profiler_support before torch_fl. Importing torch at module scope changed CUDA library initialization order and caused libc10_cuda to register graph allocator configuration twice in CUDA CI. Defer torch imports until profiler fixtures or platform detection actually run.\n\nTested: pytest tests/integration/test_profiler_contract.py -q --tb=short\nTested: ruff check tests/integration/profiler_support.py tests/integration/test_profiler_contract.py\n\nCo-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent e6122cd commit 79d5c95

10 files changed

Lines changed: 445 additions & 376 deletions

File tree

‎.github/configs/ascend.yml‎

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -106,24 +106,16 @@ integration_tests:
106106
- name: Run general factory ops
107107
command: python -m pytest tests/integration/test_factory_ops.py -v -s --tb=short
108108

109+
- name: Unified profiler contract
110+
command: >-
111+
python -m pytest tests/integration/test_profiler_contract.py
112+
-v -m profiler --tb=short
113+
114+
# Device-only contract cases skip through profiler capabilities while the
115+
# Ascend tracer exposes CPU/Trace records only. CPU/API coverage still runs.
109116
# ---------------------------------------------------------------------------
110117
# Follow-up (not part of first-version acceptance). Tracked here so the
111118
# upstream gaps not yet covered by CI are not lost:
112-
# - Ascend RNG overload coverage (normal_ / uniform_ / exponential_ /
113-
# bernoulli_ / random_ / log_normal_ / cauchy_ / geometric_ / poisson /
114-
# _standard_gamma / _sample_dirichlet / binomial / randint_like /
115-
# randperm + *_like / *.out / *.names_out variants), the native_dropout
116-
# kernel, and Ascend generator parity: RESOLVED upstream by #70, which
117-
# routes 33 RNG ops and fixes the generator-contract assertions. The full
118-
# test_rng_dispatch.py suite now runs in CI (see "Run Ascend RNG tests"
119-
# above); the prior allowlist is retired.
120119
# - Qwen3 inference/training smoke once a model is mounted on the Ascend
121120
# runner (deferred; not required for the first-version op contract).
122-
# - Ascend profiler parity: the ascend profiler path does not yet emit
123-
# device-side events (kernel / gpu_memcpy / gpu_memset / runtime /
124-
# ac2g flows); the flagos trace has only ['Trace','cpu_op'] vs the
125-
# torch-cuda baseline. test_profiler_parity.py (7 tests) is therefore
126-
# excluded from first-version CI; re-add the step once the ascend
127-
# profiler emits the expected device/runtime categories.
128-
# None of the above may be unblocked by adding a CPU fallback or faking a pass.
129121
# ---------------------------------------------------------------------------

‎.github/configs/cuda.yml‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,10 @@ integration_tests:
8282
-m "main_ops" -v -s --tb=short
8383
- name: Run general tests
8484
command: python -m pytest tests/integration/test_factory_ops.py -v -s --tb=short
85-
# Needs its own entry: `-m main_ops` above is scoped to tests/integration/ops/,
86-
# so a marked test outside that directory is silently collected as zero tests
87-
# rather than erroring.
88-
- name: Profiler parity test
85+
- name: Unified profiler contract
8986
command: >-
90-
python -m pytest tests/integration/test_profiler_parity.py
91-
-v -m main_ops --tb=short
87+
python -m pytest tests/integration/test_profiler_contract.py
88+
-v -m profiler --tb=short
9289
- name: Run inference tests
9390
command: >-
9491
python -m pytest tests/integration/test_qwen3_infer.py

‎.github/configs/dcu.yml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ integration_tests:
9999
command: python -m pytest tests/integration/test_factory_ops.py -v -s --tb=short
100100
- name: Run AMP tests
101101
command: python -m pytest tests/integration/test_amp.py -v --tb=short
102-
- name: Profiler parity test
102+
- name: Unified profiler contract
103103
command: >-
104-
python -m pytest tests/integration/test_profiler_parity.py
105-
-v -m main_ops --tb=short
104+
python -m pytest tests/integration/test_profiler_contract.py
105+
-v -m profiler --tb=short

‎.github/configs/metax.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,8 @@ integration_tests:
131131
132132
- name: Run AMP tests
133133
command: python -m pytest tests/integration/test_amp.py -v --tb=short
134+
135+
- name: Unified profiler contract
136+
command: >-
137+
python -m pytest tests/integration/test_profiler_contract.py
138+
-v -m profiler --tb=short

‎tests/integration/conftest.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
_REPO_ROOT = Path(__file__).resolve().parents[2]
2121

22+
pytest_plugins = ("profiler_support",)
23+
2224

2325
def _ensure_backend_config() -> None:
2426
"""Ensure MetaX backend config is set before importing torch_fl (if not already specified).
@@ -70,6 +72,9 @@ def pytest_configure(config):
7072
# tests/integration/ops/conftest.py, so tests using it directly under
7173
# tests/integration/ raised PytestUnknownMarkWarning -- and would hard-error
7274
# under --strict-markers. Registering it here covers the whole directory.
75+
config.addinivalue_line(
76+
"markers", "anyplatform: runs on every supported hardware platform"
77+
)
7378
config.addinivalue_line(
7479
"markers",
7580
"main_ops: representative test in the CI smoke subset "
@@ -78,6 +83,33 @@ def pytest_configure(config):
7883
config.addinivalue_line("markers", "ascend: requires Ascend NPU hardware")
7984
config.addinivalue_line("markers", "musa: requires Moore Threads MUSA hardware")
8085
config.addinivalue_line("markers", "gcu: requires Enflame GCU hardware")
86+
config.addinivalue_line(
87+
"markers", "profiler: shared public torch.profiler contract"
88+
)
89+
config.addinivalue_line(
90+
"markers", "profiler_device: requires device activity from the profiler"
91+
)
92+
config.addinivalue_line(
93+
"markers", "profiler_kernel: requires profiler kernel activities"
94+
)
95+
config.addinivalue_line(
96+
"markers", "profiler_runtime: requires profiler runtime activities"
97+
)
98+
config.addinivalue_line(
99+
"markers", "profiler_memcpy: requires profiler memcpy activities"
100+
)
101+
config.addinivalue_line(
102+
"markers", "profiler_memset: requires profiler memset activities"
103+
)
104+
config.addinivalue_line(
105+
"markers", "profiler_flow: requires CPU-to-device profiler flows"
106+
)
107+
config.addinivalue_line(
108+
"markers", "profiler_linkage: requires profiler device-time linkage"
109+
)
110+
config.addinivalue_line(
111+
"markers", "profiler_metadata: requires profiler device metadata"
112+
)
81113

82114
import torch_fl
83115

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Copyright 2026 FlagOS Contributors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Shared fixtures and helpers for the cross-backend profiler contract."""
16+
17+
import json
18+
import os
19+
import tempfile
20+
from dataclasses import dataclass
21+
from pathlib import Path
22+
23+
import pytest
24+
25+
26+
@dataclass(frozen=True)
27+
class ProfilerCapabilities:
28+
"""Observable profiler features expected from the active backend."""
29+
30+
platform: str
31+
device: bool
32+
kernel: bool
33+
runtime: bool
34+
memcpy: bool
35+
memset: bool
36+
flow: bool
37+
linkage: bool
38+
metadata: bool
39+
40+
41+
def detect_platform() -> str:
42+
"""Reuse the integration platform conventions for profiler selection."""
43+
accelerator = os.environ.get("ACCELERATOR", "").lower()
44+
if accelerator in {"ascend", "metax", "maca", "musa", "gcu"}:
45+
return "metax" if accelerator == "maca" else accelerator
46+
if os.environ.get("PPU_SDK") or os.environ.get("PPU_HOME"):
47+
return "ppu"
48+
if Path("/usr/local/PPU_SDK").is_dir():
49+
return "ppu"
50+
51+
try:
52+
import torch_fl
53+
54+
marker = Path(torch_fl.__file__).resolve().parent / "lib" / "flagos_platform"
55+
platform = marker.read_text().strip().lower()
56+
if platform:
57+
return platform
58+
except (ImportError, OSError):
59+
pass
60+
61+
config = os.environ.get("FLAGOS_BACKEND_CONFIG", "").lower()
62+
for platform in ("ascend", "metax", "musa", "gcu", "cuda"):
63+
if platform in config:
64+
return platform
65+
return "cuda"
66+
67+
68+
def _torch_device():
69+
import torch
70+
71+
return torch.device("flagos", 0)
72+
73+
74+
def _torch_module():
75+
import torch
76+
77+
return torch
78+
79+
80+
def capabilities_for_platform(platform: str) -> ProfilerCapabilities:
81+
"""Describe public profiler features currently emitted by each tracer.
82+
83+
The capability table is intentionally about observable behavior, not vendor
84+
library names. Ascend currently exposes CPU/Trace records only in CI; all
85+
other supported tracers are expected to provide kernel and runtime records.
86+
"""
87+
device = platform != "ascend"
88+
runtime = device
89+
return ProfilerCapabilities(
90+
platform=platform,
91+
device=device,
92+
kernel=device,
93+
runtime=runtime,
94+
memcpy=device and platform in {"cuda", "metax", "ppu"},
95+
memset=device and platform in {"cuda", "metax"},
96+
flow=device,
97+
linkage=device,
98+
metadata=device,
99+
)
100+
101+
102+
@pytest.fixture(scope="session")
103+
def profiler_capabilities():
104+
"""Capabilities for the active hardware/backend."""
105+
return capabilities_for_platform(detect_platform())
106+
107+
108+
@pytest.fixture(scope="module")
109+
def profile_result():
110+
"""Capture one common workload and export it as a Chrome trace."""
111+
torch = _torch_module()
112+
device = _torch_device()
113+
x = torch.randn(256, 256, device=device)
114+
y = torch.randn(256, 256, device=device)
115+
small = torch.randn(16, device=device)
116+
117+
with torch.profiler.profile(
118+
activities=[
119+
torch.profiler.ProfilerActivity.CPU,
120+
torch.profiler.ProfilerActivity.PrivateUse1,
121+
],
122+
with_stack=False,
123+
) as prof:
124+
for _ in range(3):
125+
z = (x @ y).relu()
126+
torch.sort(small)
127+
z.sum().item()
128+
129+
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as trace_file:
130+
trace_path = Path(trace_file.name)
131+
try:
132+
prof.export_chrome_trace(str(trace_path))
133+
trace = json.loads(trace_path.read_text())
134+
finally:
135+
trace_path.unlink(missing_ok=True)
136+
return prof, trace
137+
138+
139+
def events_in(trace, category, *, completed_only=True):
140+
"""Return trace events in one category."""
141+
events = [
142+
event for event in trace.get("traceEvents", []) if event.get("cat") == category
143+
]
144+
if completed_only:
145+
return [event for event in events if event.get("ph") == "X"]
146+
return events
147+
148+
149+
def event_categories(trace):
150+
"""Return categories for completed trace events."""
151+
return {
152+
event.get("cat")
153+
for event in trace.get("traceEvents", [])
154+
if event.get("ph") == "X"
155+
}
156+
157+
158+
def arg_key_union(trace, category):
159+
"""Return the union of argument keys for all events in a category."""
160+
keys = set()
161+
for event in events_in(trace, category):
162+
keys.update((event.get("args") or {}).keys())
163+
return keys
164+
165+
166+
def op_name_by_external_id(trace):
167+
"""Map Kineto External ids to CPU operation names."""
168+
return {
169+
(event.get("args") or {}).get("External id"): event.get("name")
170+
for event in events_in(trace, "cpu_op")
171+
if (event.get("args") or {}).get("External id") is not None
172+
}

0 commit comments

Comments
 (0)