# This code is a part of Fulqrum.
#
# (C) Copyright IBM 2026.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
from __future__ import annotations
import json
import logging
import math
from datetime import datetime, timezone
from pathlib import Path
_PACKAGE = "fulqrum.gpu"
_TRACE = "fulqrum.gpu.diagnostics"
_LEVELS = {
"quiet": logging.WARNING,
"summary": logging.INFO,
"detailed": logging.DEBUG,
"trace": logging.DEBUG,
}
class _TierFilter(logging.Filter):
def __init__(self, mode, rank, ranks):
super().__init__()
self.mode, self.rank, self.ranks = mode, rank, ranks
def filter(self, record):
if record.levelno >= logging.WARNING:
return True
if self.mode == "quiet":
return False
if self.mode != "trace" and (
record.name == _TRACE or record.name.startswith(_TRACE + ".")
):
return False
rank = getattr(record, "gpu_rank", self.rank)
return self.ranks is None or rank in self.ranks
def format_memory(nbytes, *, unit="GiB"):
if nbytes is None:
return "unavailable"
divisors = {"GiB": 2**30, "MiB": 2**20}
return f"{nbytes / divisors[unit]:.3f} {unit}"
def _json_value(value):
if isinstance(value, dict):
return {str(key): _json_value(item) for key, item in value.items()}
if isinstance(value, (list, tuple)):
return [_json_value(item) for item in value]
if isinstance(value, float):
return value if math.isfinite(value) else None
if value is None or isinstance(value, (str, int, bool)):
return value
if type(value).__module__.split(".")[0] == "numpy":
return _json_value(value.tolist())
raise TypeError(
f"Profile data contains an unsupported value: {type(value).__name__}"
)
def gpu_profile_record(operator):
"""Return a JSON-ready snapshot of existing per-process records; this enables no profiling
and gathers nothing across MPI ranks."""
entries = []
for index, op in enumerate(getattr(operator, "_ranks", (operator,))):
comm = getattr(op, "_comm", None)
rank = getattr(op, "_rank", None)
if rank is None:
rank = comm.Get_rank() if comm is not None else index
entry = {
"rank": int(rank),
"devices": getattr(op, "_gpu_ids", ()),
"construction": getattr(op, "construction_timing", {}),
"last_matvec": getattr(op, "last_timing", {}),
"configuration": getattr(op, "run_record", {}),
}
plan = getattr(op, "plan", None)
if plan is not None:
entry["layout"] = plan.to_dict()
entry["device_storage_bytes"] = op.device_bytes
entries.append(entry)
return _json_value(
{
"schema_version": 1,
"event": "gpu_profile_snapshot",
"timestamp": datetime.now(timezone.utc).isoformat(),
"scope": "process",
"ranks": entries,
"units": {
"bytes": "B",
"*_ms*": "ms",
"arm_ms": "ms",
"wall": "s",
"compute": "s",
"exchange": "s",
"sync": "s",
"h2d": "s",
"d2h": "s",
},
}
)
def write_gpu_profile(operator, destination):
"""Write an existing profile snapshot as JSON to a path or stream and return it. Use a
separate file per MPI process."""
record = gpu_profile_record(operator)
text = json.dumps(record, indent=2, sort_keys=True, allow_nan=False) + "\n"
if hasattr(destination, "write"):
destination.write(text)
else:
Path(destination).write_text(text, encoding="utf-8")
return record