Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/docstrings/dsl/dace/hardware_config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# hardware_config

::: dsl.dace.hardware_config

<style>
/* re-enable the left side navigation bar for this page */
@media screen and (min-width: 76.1875em) {
.md-sidebar--primary {
display: block !important;
}
}
</style>
22 changes: 15 additions & 7 deletions ndsl/dsl/dace/dace_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from ndsl.dsl import NDSL_COMPILER_SILENCE, NDSL_GLOBAL_PRECISION
from ndsl.dsl.caches.cache_location import identify_code_path
from ndsl.dsl.caches.codepath import FV3CodePath
from ndsl.dsl.dace.hardware_config import get_gpu_hardware_defaults
from ndsl.optional_imports import cupy as cp
from ndsl.performance.collector import NullPerformanceCollector, PerformanceCollector

Expand Down Expand Up @@ -272,14 +273,21 @@ def __init__(
value=f"-std=c++14 {warnings_policy} -Xcompiler -fPIC -O{optimization_level} -Xcompiler {march_option} {gpu_config.gpu_compile_flags}",
)

cuda_sm = cp.cuda.Device(0).compute_capability if cp else 60
dace.config.Config.set("compiler", "cuda", "cuda_arch", value=f"{cuda_sm}")
# Block size/thread count is defaulted to an average value for recent
# hardware (Pascal and upward). The problem of setting an optimized
# block/thread is both hardware and problem dependant. Fine tuners
# available in DaCe should be relied on for further tuning of this value.
# Target compilation for hardware micro-code capacities
gpu_defaults = get_gpu_hardware_defaults()
dace.config.Config.set(
"compiler", "cuda", "default_block_size", value="64,8,1"
"compiler",
"cuda",
"cuda_arch",
value=f"{gpu_defaults.compute_capability}",
)

# Default block size for kernels launch
dace.config.Config.set(
"compiler",
"cuda",
"default_block_size",
value=str(gpu_defaults.block_size)[1:-1],
)
# Potentially buggy - deactivate
dace.config.Config.set(
Expand Down
122 changes: 122 additions & 0 deletions ndsl/dsl/dace/hardware_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import dataclasses
import sys
from pathlib import Path
from typing import Literal

from ndsl import ndsl_log
from ndsl.optional_imports import cupy as cp


GPUVendor = Literal["Nvidia"] | Literal["AMD"] | Literal["Intel"] | Literal["Unknown"]

# Taken straight out of https://pcisig.com/membership/member-companies
_VENDOR_PCI_SIGNATURES: dict[int, GPUVendor] = {
0x10DE: "Nvidia",
0x1002: "AMD",
0x8086: "Intel",
0x0: "Unknown",
}

# Cached copy of the hardware default
_GPU_HARDWARE_DEFAULTS = None


def _get_vendor() -> GPUVendor:
"""Retrieve vendor using the current device PCI id to query the PCI vendor
from the kernel logs.

⚠️ Only works on Linux - kicks back to "Unknown" in other cases.
"""
if not sys.platform.startswith("linux"):
ndsl_log.info("GPU hardware detection only possible on Linux system.")
return "Unknown"

pci_device_id = cp.cuda.runtime.deviceGetPCIBusId(0)
dev_path = Path("/sys", "bus", "pci", "devices", f"{pci_device_id}")
if not dev_path.exists():
ndsl_log.info(f"GPU detection: PCI device not found at {dev_path}.")
return "Unknown"

with open(dev_path / "vendor", "r") as f:
vendor_str = f.read().strip().replace("0x", "")
vendor_id = int(vendor_str, 16)

if vendor_id not in _VENDOR_PCI_SIGNATURES:
ndsl_log.error(f"Unknown GPU vendor with PCI-SIG ID of {vendor_id:#X}.")
return "Unknown"

return _VENDOR_PCI_SIGNATURES[vendor_id]


@dataclasses.dataclass
class GPUHardwareDefaults:
"""Compute defaults for common GPUs."""

vendor: GPUVendor
block_size: list[int] = dataclasses.field(default_factory=list)
compute_capability: int = -1 # Nvidia specific


def get_gpu_hardware_defaults() -> GPUHardwareDefaults:
"""Retrieve default values for GPU computation configuration."""
global _GPU_HARDWARE_DEFAULTS
if _GPU_HARDWARE_DEFAULTS is not None:
return _GPU_HARDWARE_DEFAULTS # type: ignore[unreachable]

if cp is None or not cp.cuda.is_available():
ndsl_log.warning("No cupy - defaulting for GPU hardware")
_GPU_HARDWARE_DEFAULTS = GPUHardwareDefaults(
vendor="Unknown",
# Smallest common denominator of massively parallel hardware
block_size=[8, 1, 1],
)
return _GPU_HARDWARE_DEFAULTS

# Who goes there
vendor = _get_vendor()
match vendor:
case "Nvidia":
compute_capability = int(cp.cuda.Device(0).compute_capability)
# Default block size based on compute capability
if compute_capability > 80:
# Covers:
# - Blackwell (100+)
# - Hopper (90-100)
# - Ampere (80-90)
block_sizes = [128, 1, 1]
elif compute_capability > 60:
# Covers:
# - Volta (70-80)
# - Pascal (60-70)
block_sizes = [64, 8, 1]
else:
# For older hardware - we default to the safe warp-size since
# the dawn of GPGPU on Nvidia hardware
block_sizes = [32, 1, 1]

_GPU_HARDWARE_DEFAULTS = GPUHardwareDefaults(
vendor=vendor,
block_size=block_sizes,
compute_capability=compute_capability,
)
case "AMD":
_GPU_HARDWARE_DEFAULTS = GPUHardwareDefaults(
vendor=vendor,
block_size=[64, 1, 1], # Default RDNA architecture is Wave64
)
case "Intel":
_GPU_HARDWARE_DEFAULTS = GPUHardwareDefaults(
vendor=vendor,
# Intel can run 8, 16 or 32 - but SIMD betters in 32
block_size=[32, 1, 1],
)
case _:
_GPU_HARDWARE_DEFAULTS = GPUHardwareDefaults(
vendor=vendor,
# Smallest common denominator of massively parallel hardware
block_size=[8, 1, 1],
)

ndsl_log.info(f"GPU vendor detected: {_GPU_HARDWARE_DEFAULTS.vendor}")

return _GPU_HARDWARE_DEFAULTS
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ module = [

[tool.pytest]
markers = [
# tests running on a cpu (e.g. with cupy)
# tests running on a GPU (e.g. with cupy)
"gpu",
# tests relying on the optional pyFMS dependency
"pyfms",
Expand Down
20 changes: 20 additions & 0 deletions tests/dsl/dace/test_hardware_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import pytest

from ndsl.dsl.dace.hardware_config import get_gpu_hardware_defaults
from ndsl.optional_imports import cupy


@pytest.mark.gpu
def test_gpu_detection() -> None:
assert cupy is not None

defaults = get_gpu_hardware_defaults()
assert defaults.vendor != "Unknown"


def test_gpu_detection_no_crash_on_cpu() -> None:
if cupy is not None:
pytest.skip("This test only make sense when access to GPU is not available.")

defaults = get_gpu_hardware_defaults()
assert defaults.vendor == "Unknown"
1 change: 1 addition & 0 deletions zensical.toml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ nav = [
{"build" = "docstrings/dsl/dace/build.md"},
{"dace_config" = "docstrings/dsl/dace/dace_config.md"},
{"dace_executable" = "docstrings/dsl/dace/dace_executable.md"},
{"hardware_config" = "docstrings/dsl/dace/hardware_config.md"},
{"labeler" = "docstrings/dsl/dace/labeler.md"},
{"orchestration" = "docstrings/dsl/dace/orchestration.md"},
{"replacements" = "docstrings/dsl/dace/replacements.md"},
Expand Down