Skip to content
Open
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
8 changes: 8 additions & 0 deletions docs/hardware.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ bits 18:11 of SMN `0x5D448`; the desktop fuse address reads `0xFFFFFFFF` there.
5600GE (issue #18) confirmed it: the register fused off slots 0 and 1, the same two
slots whose PM table rows read all zero, so its cores 0-5 are slots 2-7.

On Vermeer, Linux can also number a surviving physical CCD 1 as CCD 0. The
driver reads the CCD-disable fuses at `0x5D218` and `0x5D21C` to map L3 groups
onto active physical CCDs before reading their core fuses or sending CO commands.
For example, a 5600X with physical CCD 0 disabled uses the core fuse at
`0x32081D98`, whose mask `0x0C` leaves slots 0, 1, 4, 5, 6, and 7 active.
This follows [ryzen_smu's CCD fuse decoding](https://github.com/amkillam/ryzen_smu/blob/d2983668300dd2a598e5a7dc40e71ce0678cc270/userspace/monitor_cpu.c#L342).
Per-core CO stays disabled if the active CCD count disagrees with the OS.

Reading an SMN register is a *write* of the address, so this needs write access to
`/sys/kernel/ryzen_smu_drv/smn`, which `ryzen_smu` ships root-only -- the NixOS
module grants it to the `corecycler` group alongside the mailbox files, and
Expand Down
7 changes: 7 additions & 0 deletions src/corecycler/smu/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ class SMUCommandSet:
core_fuse_shift: int = 0
core_fuse_die_wide: bool = False

# Zen 3 chiplets can retain physical CCD 1 while Linux numbers it 0.
# CCDs 0/1 are disabled by bits 31:30 here, CCDs 2-7 by bits 5:0
# of the next register, as in ryzen_smu's get_fuse_topology.
ccd_fuse_addr: int | None = None

# CO (Curve Optimizer / DldoPsmMargin) commands — None if generation lacks CO.
# get_co_mailbox overrides the mailbox for the GET command only: the APU
# classes set CO via MP1 but read it back via RSMU (ZenStates-Core
Expand Down Expand Up @@ -200,6 +205,7 @@ def has_pbo_limits(self) -> bool:
encoding_scheme="zen3",
uniform_8core_ccds=True,
core_fuse_addr=_FUSE_ZEN3_CLASS,
ccd_fuse_addr=0x5D218,
set_co_cmd=0x35,
set_all_co_cmd=0x36,
get_co_cmd=0x48,
Expand Down Expand Up @@ -232,6 +238,7 @@ def has_pbo_limits(self) -> bool:
encoding_scheme="zen3",
uniform_8core_ccds=True,
core_fuse_addr=_FUSE_ZEN3_CLASS,
ccd_fuse_addr=0x5D218,
set_co_cmd=0x35,
set_all_co_cmd=0x36,
get_co_cmd=0x48,
Expand Down
28 changes: 26 additions & 2 deletions src/corecycler/smu/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ def set_topology(self, topology) -> None:

Mapping rules, per L3 group (falling back to ``core_id // 8`` grouping
when L3 info is absent):
- On Vermeer, the CCD-disable fuses map Linux's compact L3 groups
onto physical CCDs before reading core fuses or encoding commands.
- A full 8-core group maps to identity slots under any numbering
scheme -- no SMU traffic.
- A group with a hole INTERNAL to its own 8-slot window proves the
Expand Down Expand Up @@ -219,8 +221,10 @@ def set_topology(self, topology) -> None:
self._offline_hint = "" if fully_online else " (some present CPUs are offline — online all cores for CO tuning)"
core_map: dict[int, tuple[int, int]] = {}
try:
for encode_ccd in sorted(groups):
group_ids = groups[encode_ccd]
physical_ccds = self._physical_ccd_map(groups)
for logical_ccd in sorted(groups):
encode_ccd = physical_ccds[logical_ccd]
group_ids = groups[logical_ccd]
slots = self._derive_group_slots(group_ids, fully_online)
if slots is None:
slots = self._fuse_group_slots(encode_ccd, len(group_ids))
Expand All @@ -232,6 +236,26 @@ def set_topology(self, topology) -> None:
self._core_map = core_map
self._core_map_error = None

def _physical_ccd_map(self, groups: dict[int, list[int]]) -> dict[int, int]:
addr = self.commands.ccd_fuse_addr
if addr is None:
return {ccd: ccd for ccd in groups}
ok, message = self.check_smn_readable()
if not ok:
raise CoreMapError(f"CCD/core-disable fuse discovery requires SMN access: {message}")
present = self.read_smn(addr)
down = self.read_smn(addr + 4)
if present is None or down is None:
raise CoreMapError("CCD fuse read failed; per-core CO stays disabled")
disabled = ((present >> 30) & 3) | ((down & 0x3F) << 2)
physical = [ccd for ccd in range(8) if not (disabled >> ccd) & 1]
if len(physical) != len(groups):
raise CoreMapError(
f"CCD fuses report active physical CCDs {physical} but the OS reports "
f"{len(groups)} CCD groups; per-core CO stays disabled{self._offline_hint}"
)
return dict(zip(sorted(groups), physical, strict=True))

@staticmethod
def _derive_group_slots(group_ids: list[int], holes_trusted: bool) -> list[int] | None:
"""Slots this group's numbering proves by itself; None means read the fuse.
Expand Down
7 changes: 6 additions & 1 deletion tests/contract_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ def _pin_core_slot_mapping() -> None:
vermeer = get_commands(CPUGeneration.ZEN3_VERMEER)
assert vermeer is not None and vermeer.uniform_8core_ccds
assert vermeer.core_fuse_addr == 0x30081D98
assert vermeer.ccd_fuse_addr == 0x5D218
smu = RyzenSMU(vermeer, Path("/nonexistent"))
topo = MagicMock()
topo.cores = {c: MagicMock(ccd=0) for c in range(6)}
Expand All @@ -272,14 +273,18 @@ def responder(cmd, args=(0, 0, 0, 0, 0, 0)):

def read_smn(address: int) -> int:
fuse_reads.append(address)
if address == 0x5D218:
return 0x80400000
if address == 0x5D21C:
return 0x3F
return sum(1 << slot for slot in fused_off)

smu._send_command = responder
smu.check_smn_readable = lambda: (True, "OK")
smu.read_smn = read_smn
smu.set_topology(topo)
assert co_calls == []
assert fuse_reads == [vermeer.core_fuse_addr]
assert fuse_reads == [0x5D218, 0x5D21C, vermeer.core_fuse_addr]
assert smu.core_map == {0: (0, 0), 1: (0, 1), 2: (0, 4), 3: (0, 5), 4: (0, 6), 5: (0, 7)}
fused = {
gen: (cmds.core_fuse_addr, cmds.core_fuse_shift) for gen, cmds in COMMAND_SETS.items() if cmds.core_fuse_addr
Expand Down
56 changes: 56 additions & 0 deletions tests/test_smu_ccd_map.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Vermeer can expose physical CCD 1 as Linux's only L3 group, numbered 0."""

from types import SimpleNamespace
from unittest.mock import MagicMock

from corecycler.smu.commands import CPUGeneration, get_commands
from corecycler.smu.driver import RyzenSMU

VERMEER = get_commands(CPUGeneration.ZEN3_VERMEER)


def _discover(cores: dict[int, int], words: dict[int, int]) -> RyzenSMU:
smu = RyzenSMU(VERMEER, MagicMock())
smu.check_smn_readable = lambda: (True, "OK")
smu.read_smn = words.get
smu.set_topology(SimpleNamespace(cores={cid: SimpleNamespace(ccd=ccd) for cid, ccd in cores.items()}))
return smu


def test_reported_5600x_uses_the_surviving_second_ccd() -> None:
smu = _discover(
dict.fromkeys(range(6), 0),
{0x5D218: 0x40C00000, 0x5D21C: 0x3F, 0x30081D98: 0x3FF, 0x32081D98: 0x30C},
)
assert smu.core_map_error is None
assert smu.core_map == {0: (1, 0), 1: (1, 1), 2: (1, 4), 3: (1, 5), 4: (1, 6), 5: (1, 7)}


def test_full_single_ccd_still_needs_its_physical_ccd_number() -> None:
smu = _discover(
dict.fromkeys(range(8), 0),
{0x5D218: 0x40C00000, 0x5D21C: 0x3F, 0x30081D98: 0x3FF, 0x32081D98: 0x300},
)
assert smu.core_map_error is None
assert smu.core_map == {cid: (1, cid) for cid in range(8)}


def test_5950x_preserves_both_full_ccds() -> None:
smu = _discover(
{cid: cid // 8 for cid in range(16)},
{0x5D218: 0x00C00000, 0x5D21C: 0x3F, 0x30081D98: 0x300, 0x32081D98: 0x300},
)
assert smu.core_map_error is None
assert smu.core_map == {cid: (cid // 8, cid % 8) for cid in range(16)}


def test_ccd_count_disagreement_refuses_to_guess() -> None:
smu = _discover(dict.fromkeys(range(6), 0), {0x5D218: 0x00C00000, 0x5D21C: 0x3F})
assert smu.core_map is None
assert "active physical CCDs [0, 1]" in smu.core_map_error


def test_unreadable_ccd_fuses_refuse_to_guess() -> None:
smu = _discover(dict.fromkeys(range(8), 0), {})
assert smu.core_map is None
assert "CCD fuse read failed" in smu.core_map_error
13 changes: 13 additions & 0 deletions tests/test_smu_core_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ def __init__(self, smu: RyzenSMU, commands, live: dict[int, set[int]], *, smn: b
smu.read_smn = self._read_smn

def _read_smn(self, address: int) -> int | None:
if self.commands.ccd_fuse_addr is not None:
enabled = sum(1 << ccd for ccd in self.live) or 1
disabled = (~enabled) & 0xFF
if address == self.commands.ccd_fuse_addr:
return ((enabled & 3) << 22) | ((disabled & 3) << 30)
if address == self.commands.ccd_fuse_addr + 4:
return disabled >> 2
self.fuse_reads.append(address)
base = self.commands.core_fuse_addr
ccd = (address - base) >> 25
Expand Down Expand Up @@ -113,6 +120,12 @@ def _fuse_addr(commands, ccd: int) -> int:


class TestRenumberedHarvested:
def test_write_uses_surviving_physical_ccd_and_skips_disabled_slots(self) -> None:
smu, silicon = _mapped_smu(VERMEER, dict.fromkeys(range(6), 0), {1: REPORTED_5600X_LIVE_SLOTS})
assert smu.set_co_offset(2, -1) is True
assert silicon.writes == [(1, 4, -1)]
assert smu.get_co_offset(2) == -1

def test_5600x_renumbered_maps_onto_live_slots(self):
smu, silicon = _mapped_smu(
VERMEER,
Expand Down