From 149edb8aec61ca42b17c30ca4daeac26e17c9d15 Mon Sep 17 00:00:00 2001 From: Xiaodong Ye Date: Fri, 24 Jul 2026 18:04:29 +0800 Subject: [PATCH 1/4] Prefer versioned MTML library on Linux --- README.md | 3 ++- pymtml.py | 31 ++++++++++++---------- test_library_loading.py | 57 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 test_library_loading.py diff --git a/README.md b/README.md index fa78a40..1c59829 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ Moore Threads GPUs use MUSA (Meta-computing Unified System Architecture) as thei - Python 3.7+ - Moore Threads GPU driver with MTML library installed -- **Linux**: The `libmtml.so` shared library must be in the library path +- **Linux**: `libmtml.so.2` (preferred) or the legacy `libmtml.so` must be in + the library path - **Windows**: The `mtml.dll` library must be in the system PATH or in the `mtml/` directory relative to the script ## Installation diff --git a/pymtml.py b/pymtml.py index 0239bc7..76badff 100644 --- a/pymtml.py +++ b/pymtml.py @@ -662,20 +662,25 @@ def _LoadLinuxLibrary(): """ Load the MTML library on Linux/Unix platform """ - lib_name = "libmtml.so" lib_loader = CDLL - - # Try loading the library with different names - last_error = None - try: - return lib_loader(lib_name) - except OSError as ose: - last_error = ose - - # If failed, raise detailed error - error_msg = f"Failed to load MTML library on Linux. Tried: {lib_name}" - if last_error: - error_msg += f"\nLast error: {last_error}" + lib_names = ("libmtml.so.2", "libmtml.so") + load_errors = [] + + # Prefer the versioned runtime shipped by current MUSA images. Older + # host/runtime installations expose only libmtml.so, so retain it as a + # fallback for backwards compatibility. + for lib_name in lib_names: + try: + return lib_loader(lib_name) + except OSError as ose: + load_errors.append(f"{lib_name}: {ose}") + + error_msg = ( + "Failed to load MTML library on Linux. " + f"Tried: {', '.join(lib_names)}" + ) + if load_errors: + error_msg += "\nLoad errors:\n" + "\n".join(load_errors) raise OSError(error_msg) ## C function wrappers ## diff --git a/test_library_loading.py b/test_library_loading.py new file mode 100644 index 0000000..f79a7e3 --- /dev/null +++ b/test_library_loading.py @@ -0,0 +1,57 @@ +"""Unit tests for MTML shared-library selection.""" + +import pytest + +import pymtml + + +def test_linux_loader_prefers_versioned_library(monkeypatch): + loaded = object() + calls = [] + + def fake_cdll(name): + calls.append(name) + return loaded + + monkeypatch.setattr(pymtml, "CDLL", fake_cdll) + + assert pymtml._LoadLinuxLibrary() is loaded + assert calls == ["libmtml.so.2"] + + +def test_linux_loader_falls_back_to_legacy_library(monkeypatch): + loaded = object() + calls = [] + + def fake_cdll(name): + calls.append(name) + if name == "libmtml.so.2": + raise OSError("versioned library is unavailable") + return loaded + + monkeypatch.setattr(pymtml, "CDLL", fake_cdll) + + assert pymtml._LoadLinuxLibrary() is loaded + assert calls == ["libmtml.so.2", "libmtml.so"] + + +def test_linux_loader_reports_both_failures(monkeypatch): + calls = [] + + def fake_cdll(name): + calls.append(name) + if name == "libmtml.so.2": + raise OSError("versioned load failed") + raise OSError("legacy load failed") + + monkeypatch.setattr(pymtml, "CDLL", fake_cdll) + + with pytest.raises(OSError) as exc_info: + pymtml._LoadLinuxLibrary() + + message = str(exc_info.value) + assert calls == ["libmtml.so.2", "libmtml.so"] + assert "libmtml.so.2" in message + assert "versioned load failed" in message + assert "libmtml.so" in message + assert "legacy load failed" in message From a1f17d2d980836b3eb13bb19ac1e7dbf43398853 Mon Sep 17 00:00:00 2001 From: Xiaodong Ye Date: Fri, 24 Jul 2026 18:07:27 +0800 Subject: [PATCH 2/4] Fix ctypes layouts for MTML 2.x ABI --- pymtml.py | 328 +++++++++++++++++++++++++++++++++++++-------- test_abi_layout.py | 200 +++++++++++++++++++++++++++ 2 files changed, 474 insertions(+), 54 deletions(-) create mode 100644 test_abi_layout.py diff --git a/pymtml.py b/pymtml.py index 76badff..fd66c8a 100644 --- a/pymtml.py +++ b/pymtml.py @@ -91,7 +91,7 @@ MTML_CODEC_TYPE_AV1 = 16 MTML_CODEC_TYPE_COUNT = 17 -_mtmlCodecSessionState_t = c_uint +_mtmlCodecSessionState_t = c_int MTML_CODEC_SESSION_STATE_UNKNOWN = -1 MTML_CODEC_SESSION_STATE_IDLE = 0 MTML_CODEC_SESSION_STATE_ACTIVE = 1 @@ -191,6 +191,19 @@ MTML_MTLINK_STATE_UP = 1 MTML_MTLINK_STATE_DOWNGRADE = 2 +_mtmlMtLinkCap_t = c_uint +MTML_MTLINK_CAP_P2P_ACCESS = 0 +MTML_MTLINK_CAP_P2P_ATOMICS = 1 +MTML_MTLINK_CAP_COUNT = 2 + +_mtmlMtLinkCapStatus_t = c_uint +MTML_MTLINK_CAP_STATUS_NOT_SUPPORTED = 0 +MTML_MTLINK_CAP_STATUS_OK = 1 + +_mtmlMtLinkCapability_t = c_uint +MTML_DEVICE_NOT_SUPPORT_MTLINK = 0 +MTML_DEVICE_SUPPORT_MTLINK = 1 + ## Library structures class struct_c_mtmlLibrary_t(Structure): @@ -340,6 +353,7 @@ class c_mtmlPciInfo_t(_PrintableStructure): ("bus", c_uint), ("device", c_uint), ("pciDeviceId", c_uint), + ("pciSubsystemId", c_uint), ("busWidth", c_uint), ("pciMaxSpeed", c_float), ("pciCurSpeed", c_float), @@ -347,30 +361,63 @@ class c_mtmlPciInfo_t(_PrintableStructure): ("pciCurWidth", c_uint), ("pciMaxGen", c_uint), ("pciCurGen", c_uint), - ("busId", c_char * MTML_DEVICE_PCI_BUS_ID_BUFFER_SIZE), - ("rsvd", c_uint * 6), + ("rsvd", c_int * 6), ] + @property + def busId(self): + return self.sbdf + ## Device property structure class c_mtmlDeviceProperty_t(_PrintableStructure): _fields_ = [ - ("virtCapability", c_uint), - ("virtRole", c_uint), - ("mpcCapability", c_uint), - ("mpcType", c_uint), - ("rsvd", c_uint * 12), + ("virtCap", c_uint, 1), + ("virtRole", c_uint, 3), + ("mpcCap", c_uint, 1), + ("mpcType", c_uint, 3), + ("mtLinkCap", c_uint, 1), + ("rsvd", c_uint, 23), + ("rsvd2", c_uint, 32), ] + @property + def virtCapability(self): + return self.virtCap + + @virtCapability.setter + def virtCapability(self, value): + self.virtCap = value + + @property + def mpcCapability(self): + return self.mpcCap + + @mpcCapability.setter + def mpcCapability(self, value): + self.mpcCap = value + + @property + def mtLinkCapability(self): + return self.mtLinkCap + + @mtLinkCapability.setter + def mtLinkCapability(self, value): + self.mtLinkCap = value + ## PCI slot info structure class c_mtmlPciSlotInfo_t(_PrintableStructure): _fields_ = [ - ("slotType", c_uint), + ("slotId", c_uint), ("slotName", c_char * MTML_DEVICE_SLOT_NAME_BUFFER_SIZE), ("rsvd", c_uint * 4), ] + @property + def slotType(self): + return self.slotId + ## Display interface spec structure class c_mtmlDispIntfSpec_t(_PrintableStructure): @@ -378,88 +425,235 @@ class c_mtmlDispIntfSpec_t(_PrintableStructure): ("type", c_uint), ("maxResWidth", c_uint), ("maxResHeight", c_uint), - ("rsvd", c_uint * 4), + ("maxRefreshRate", c_float), + ("rsvd", c_uint * 8), ] + @property + def maxHoriRes(self): + return self.maxResWidth + + @property + def maxVertRes(self): + return self.maxResHeight + ## Virtualization type structure class c_mtmlVirtType_t(_PrintableStructure): _fields_ = [ ("id", c_char * MTML_VIRT_TYPE_ID_BUFFER_SIZE), - ("deviceClass", c_char * MTML_VIRT_TYPE_CLASS_BUFFER_SIZE), ("name", c_char * MTML_VIRT_TYPE_NAME_BUFFER_SIZE), + ("api", c_char * MTML_VIRT_TYPE_API_BUFFER_SIZE), + ("horizontalResolution", c_uint), + ("verticalResolution", c_uint), + ("frameBuffer", c_uint), + ("maxEncodeNum", c_uint), + ("maxDecodeNum", c_uint), ("maxInstances", c_uint), - ("memSize", c_ulonglong), - ("gpuCores", c_uint), - ("maxResWidth", c_uint), - ("maxResHeight", c_uint), - ("apiType", c_char * MTML_VIRT_TYPE_API_BUFFER_SIZE), - ("encoderNum", c_uint), - ("decoderNum", c_uint), - ("rsvd", c_uint * 4), + ("maxVirtualDisplay", c_uint), + ("rsvd", c_int * 11), ] + @property + def deviceClass(self): + return b"" + + @property + def apiType(self): + return self.api + + @property + def maxResWidth(self): + return self.horizontalResolution + + @property + def maxResHeight(self): + return self.verticalResolution + + @property + def memSize(self): + return self.frameBuffer * 1024 * 1024 + + @property + def gpuCores(self): + return 0 + + @property + def encoderNum(self): + return self.maxEncodeNum + + @property + def decoderNum(self): + return self.maxDecodeNum + ## Codec utilization structure class c_mtmlCodecUtil_t(_PrintableStructure): _fields_ = [ - ("encodeUtil", c_uint), - ("decodeUtil", c_uint), - ("rsvd", c_uint * 4), + ("util", c_uint), + ("period", c_uint), + ("encUtil", c_uint), + ("decUtil", c_uint), + ("rsvd", c_int * 2), ] + @property + def encodeUtil(self): + return self.encUtil + + @property + def decodeUtil(self): + return self.decUtil + ## Codec session state structure -class c_mtmlCodecSessionState_t(_PrintableStructure): - _fields_ = [ - ("sessionId", c_uint), - ("state", c_uint), - ("rsvd", c_uint * 4), - ] +class c_mtmlCodecSessionState_t(c_int): + pass ## Codec session metrics structure class c_mtmlCodecSessionMetrics_t(_PrintableStructure): _fields_ = [ - ("width", c_uint), - ("height", c_uint), + ("id", c_uint), + ("pid", c_uint), + ("hResolution", c_uint), + ("vResolution", c_uint), + ("frameRate", c_uint), + ("bitRate", c_uint), + ("latency", c_uint), ("codecType", c_uint), - ("fps", c_uint), - ("rsvd", c_uint * 4), + ("rsvd", c_int * 4), + ] + + @property + def sessionId(self): + return self.id + + @property + def width(self): + return self.hResolution + + @property + def height(self): + return self.vResolution + + @property + def fps(self): + return self.frameRate + + +class c_mtmlLogConsoleConfiguration_t(_PrintableStructure): + _fields_ = [ + ("level", c_uint), + ("rsvd", c_int * 2), + ] + + +class c_mtmlLogSystemConfiguration_t(_PrintableStructure): + _fields_ = [ + ("level", c_uint), + ("rsvd", c_int * 2), + ] + + +class c_mtmlLogFileConfiguration_t(_PrintableStructure): + _fields_ = [ + ("level", c_uint), + ("file", c_char * MTML_LOG_FILE_PATH_BUFFER_SIZE), + ("size", c_uint), + ("rsvd", c_int * 2), + ] + + +_mtmlLogCallback_t = CFUNCTYPE(None, c_char_p, c_uint) + + +class c_mtmlLogCallbackConfiguration_t(_PrintableStructure): + _fields_ = [ + ("level", c_uint), + ("callback", _mtmlLogCallback_t), + ("rsvd", c_int * 2), ] ## Log configuration structure class c_mtmlLogConfiguration_t(_PrintableStructure): _fields_ = [ - ("filePath", c_char * MTML_LOG_FILE_PATH_BUFFER_SIZE), - ("maxSize", c_uint), - ("logLevel", c_uint), - ("rsvd", c_uint * 4), + ("consoleConfig", c_mtmlLogConsoleConfiguration_t), + ("systemConfig", c_mtmlLogSystemConfiguration_t), + ("fileConfig", c_mtmlLogFileConfiguration_t), + ("callbackConfig", c_mtmlLogCallbackConfiguration_t), + ("rsvd", c_int * 8), ] + @property + def filePath(self): + return self.fileConfig.file + + @filePath.setter + def filePath(self, value): + self.fileConfig.file = value + + @property + def maxSize(self): + return self.fileConfig.size + + @maxSize.setter + def maxSize(self, value): + self.fileConfig.size = value + + @property + def logLevel(self): + return self.fileConfig.level + + @logLevel.setter + def logLevel(self, value): + self.fileConfig.level = value + ## MPC profile structure class c_mtmlMpcProfile_t(_PrintableStructure): _fields_ = [ - ("profileId", c_uint), + ("id", c_uint), + ("coreCount", c_uint), + ("memorySizeMB", c_ulonglong), ("name", c_char * MTML_MPC_PROFILE_NAME_BUFFER_SIZE), - ("memSize", c_ulonglong), - ("gpuCores", c_uint), - ("rsvd", c_uint * 4), + ("rsvd", c_uint * 10), ] + @property + def profileId(self): + return self.id + + @property + def gpuCores(self): + return self.coreCount + + @property + def memSize(self): + return self.memorySizeMB * 1024 * 1024 + ## MPC configuration structure class c_mtmlMpcConfiguration_t(_PrintableStructure): _fields_ = [ ("id", c_uint), ("name", c_char * MTML_MPC_CONF_NAME_BUFFER_SIZE), - ("profileNum", c_uint), - ("profileIds", c_uint * MTML_MPC_CONF_MAX_PROF_NUM), - ("rsvd", c_uint * 4), + ("profileId", c_int * MTML_MPC_CONF_MAX_PROF_NUM), + ("rsvd", c_uint * 24), ] + @property + def profileIds(self): + return self.profileId + + @property + def profileNum(self): + for index, profile_id in enumerate(self.profileId): + if profile_id < 0: + return index + return len(self.profileId) + ## MtLink layout structure class c_mtmlMtLinkLayout_t(_PrintableStructure): @@ -473,20 +667,44 @@ class c_mtmlMtLinkLayout_t(_PrintableStructure): ## Page retirement count structure class c_mtmlPageRetirementCount_t(_PrintableStructure): _fields_ = [ - ("singleBitEcc", c_uint), - ("doubleBitEcc", c_uint), - ("rsvd", c_uint * 4), + ("sbeCount", c_uint), + ("dbeCount", c_uint), ] + @property + def singleBitEcc(self): + return self.sbeCount + + @property + def doubleBitEcc(self): + return self.dbeCount + ## Page retirement structure class c_mtmlPageRetirement_t(_PrintableStructure): _fields_ = [ + ("timestamps", c_ulonglong), ("address", c_ulonglong), - ("timestamp", c_ulonglong), - ("rsvd", c_uint * 4), + ("rsvd", c_uint * 10), + ] + + @property + def timestamp(self): + return self.timestamps + + +class c_mtmlPageRetirementPending_t(_PrintableStructure): + _fields_ = [ + ("cause", c_uint), + ("timestamps", c_ulonglong), + ("address", c_ulonglong), + ("rsvd", c_uint * 10), ] + @property + def timestamp(self): + return self.timestamps + ## Lib loading ## mtmlLib = None @@ -868,10 +1086,6 @@ def mtmlDeviceGetPciInfo(device): fn = _mtmlGetFunctionPointer("mtmlDeviceGetPciInfo") ret = fn(device, byref(c_pciinfo)) _mtmlCheckReturn(ret) - # If busId is empty or invalid (contains non-printable chars), fill it with sbdf - bus_id = c_pciinfo.busId - if not bus_id or not bus_id[0].isalnum(): - c_pciinfo.busId = c_pciinfo.sbdf return c_pciinfo @convertStrBytes @@ -1434,7 +1648,10 @@ def mtmlVpuGetEncoderSessionStates(vpu, length): fn = _mtmlGetFunctionPointer("mtmlVpuGetEncoderSessionStates") ret = fn(vpu, c_states, c_uint(length)) _mtmlCheckReturn(ret) - return list(c_states) + return [ + mtmlFriendlyObject({"sessionId": index, "state": state.value}) + for index, state in enumerate(c_states) + ] def mtmlVpuGetEncoderSessionMetrics(vpu, sessionId): @@ -1450,7 +1667,10 @@ def mtmlVpuGetDecoderSessionStates(vpu, length): fn = _mtmlGetFunctionPointer("mtmlVpuGetDecoderSessionStates") ret = fn(vpu, c_states, c_uint(length)) _mtmlCheckReturn(ret) - return list(c_states) + return [ + mtmlFriendlyObject({"sessionId": index, "state": state.value}) + for index, state in enumerate(c_states) + ] def mtmlVpuGetDecoderSessionMetrics(vpu, sessionId): diff --git a/test_abi_layout.py b/test_abi_layout.py new file mode 100644 index 0000000..c974864 --- /dev/null +++ b/test_abi_layout.py @@ -0,0 +1,200 @@ +"""ctypes ABI layout checks against the bundled MTML 2.x header.""" + +from ctypes import alignment, c_uint, sizeof + +import pymtml + + +def test_mtml_structure_sizes_match_header_abi(): + expected_sizes = { + pymtml.c_mtmlMtLinkSpec_t: 28, + pymtml.c_mtmlPciSlotInfo_t: 52, + pymtml.c_mtmlDeviceProperty_t: 8, + pymtml.c_mtmlDispIntfSpec_t: 48, + pymtml.c_mtmlVirtType_t: 136, + pymtml.c_mtmlCodecUtil_t: 24, + pymtml.c_mtmlCodecSessionState_t: 4, + pymtml.c_mtmlCodecSessionMetrics_t: 48, + pymtml.c_mtmlLogConsoleConfiguration_t: 12, + pymtml.c_mtmlLogSystemConfiguration_t: 12, + pymtml.c_mtmlLogFileConfiguration_t: 216, + pymtml.c_mtmlLogCallbackConfiguration_t: 24, + pymtml.c_mtmlLogConfiguration_t: 296, + pymtml.c_mtmlMpcProfile_t: 88, + pymtml.c_mtmlMpcConfiguration_t: 196, + pymtml.c_mtmlMtLinkLayout_t: 24, + pymtml.c_mtmlPageRetirementCount_t: 8, + pymtml.c_mtmlPageRetirement_t: 56, + pymtml.c_mtmlPageRetirementPending_t: 64, + } + + for structure, expected_size in expected_sizes.items(): + assert sizeof(structure) == expected_size, structure.__name__ + + +def test_mtml_structure_alignments_match_header_abi(): + expected_alignments = { + pymtml.c_mtmlMtLinkSpec_t: 4, + pymtml.c_mtmlPciInfo_t: 4, + pymtml.c_mtmlPciSlotInfo_t: 4, + pymtml.c_mtmlDeviceProperty_t: 4, + pymtml.c_mtmlDispIntfSpec_t: 4, + pymtml.c_mtmlVirtType_t: 4, + pymtml.c_mtmlCodecUtil_t: 4, + pymtml.c_mtmlCodecSessionState_t: 4, + pymtml.c_mtmlCodecSessionMetrics_t: 4, + pymtml.c_mtmlLogConsoleConfiguration_t: 4, + pymtml.c_mtmlLogSystemConfiguration_t: 4, + pymtml.c_mtmlLogFileConfiguration_t: 4, + pymtml.c_mtmlLogCallbackConfiguration_t: 8, + pymtml.c_mtmlLogConfiguration_t: 8, + pymtml.c_mtmlMpcProfile_t: 8, + pymtml.c_mtmlMpcConfiguration_t: 4, + pymtml.c_mtmlMtLinkLayout_t: 4, + pymtml.c_mtmlPageRetirementCount_t: 4, + pymtml.c_mtmlPageRetirement_t: 8, + pymtml.c_mtmlPageRetirementPending_t: 8, + } + + for structure, expected_alignment in expected_alignments.items(): + assert alignment(structure) == expected_alignment, structure.__name__ + + +def test_mtml_structure_offsets_match_header_abi(): + expected_offsets = { + pymtml.c_mtmlMtLinkSpec_t: {"bandWidth": 4, "linkNum": 8, "rsvd": 12}, + pymtml.c_mtmlPciInfo_t: { + "pciSubsystemId": 48, + "busWidth": 52, + "pciMaxSpeed": 56, + "rsvd": 80, + }, + pymtml.c_mtmlPciSlotInfo_t: {"slotName": 4, "rsvd": 36}, + pymtml.c_mtmlDeviceProperty_t: {"rsvd2": 4}, + pymtml.c_mtmlDispIntfSpec_t: {"maxRefreshRate": 12, "rsvd": 16}, + pymtml.c_mtmlVirtType_t: { + "name": 16, + "api": 48, + "frameBuffer": 72, + "maxInstances": 84, + "rsvd": 92, + }, + pymtml.c_mtmlCodecUtil_t: {"encUtil": 8, "rsvd": 16}, + pymtml.c_mtmlCodecSessionMetrics_t: { + "hResolution": 8, + "codecType": 28, + "rsvd": 32, + }, + pymtml.c_mtmlLogConfiguration_t: { + "systemConfig": 12, + "fileConfig": 24, + "callbackConfig": 240, + "rsvd": 264, + }, + pymtml.c_mtmlLogFileConfiguration_t: { + "file": 4, + "size": 204, + "rsvd": 208, + }, + pymtml.c_mtmlLogCallbackConfiguration_t: {"callback": 8, "rsvd": 16}, + pymtml.c_mtmlMpcProfile_t: { + "memorySizeMB": 8, + "name": 16, + "rsvd": 48, + }, + pymtml.c_mtmlMpcConfiguration_t: {"profileId": 36, "rsvd": 100}, + pymtml.c_mtmlMtLinkLayout_t: {"remoteLinkId": 4, "rsvd": 8}, + pymtml.c_mtmlPageRetirementCount_t: {"dbeCount": 4}, + pymtml.c_mtmlPageRetirement_t: {"address": 8, "rsvd": 16}, + pymtml.c_mtmlPageRetirementPending_t: { + "timestamps": 8, + "address": 16, + "rsvd": 24, + }, + } + + for structure, field_offsets in expected_offsets.items(): + for field_name, expected_offset in field_offsets.items(): + assert getattr(structure, field_name).offset == expected_offset + + +def test_pci_info_matches_header_and_exposes_bus_id_alias(): + assert pymtml.c_mtmlPciInfo_t.pciSubsystemId.offset == 48 + assert pymtml.c_mtmlPciInfo_t.busWidth.offset == 52 + assert pymtml.c_mtmlPciInfo_t.rsvd.offset == 80 + assert sizeof(pymtml.c_mtmlPciInfo_t) == 104 + + pci_info = pymtml.c_mtmlPciInfo_t() + pci_info.sbdf = b"00000000:01:00.0" + assert pci_info.busId == pci_info.sbdf + + +def test_compatibility_aliases_use_header_backing_fields(): + prop = pymtml.c_mtmlDeviceProperty_t() + prop.virtCapability = 1 + prop.virtRole = 2 + prop.mpcCapability = 1 + prop.mpcType = 2 + prop.mtLinkCapability = 1 + assert (prop.virtCapability, prop.virtRole) == (1, 2) + assert (prop.mpcCapability, prop.mpcType) == (1, 2) + assert prop.mtLinkCapability == 1 + assert c_uint.from_buffer_copy(prop).value == 0x155 + + profile = pymtml.c_mtmlMpcProfile_t() + profile.id = 7 + profile.coreCount = 12 + profile.memorySizeMB = 4096 + assert profile.profileId == 7 + assert profile.gpuCores == 12 + assert profile.memSize == 4096 * 1024 * 1024 + + retired = pymtml.c_mtmlPageRetirementCount_t() + retired.sbeCount = 3 + retired.dbeCount = 4 + assert retired.singleBitEcc == 3 + assert retired.doubleBitEcc == 4 + + +def test_device_property_bitfield_masks_match_header_abi(): + fields = { + "virtCap": (1, 0x1), + "virtRole": (7, 0xE), + "mpcCap": (1, 0x10), + "mpcType": (7, 0xE0), + "mtLinkCap": (1, 0x100), + "rsvd": ((1 << 23) - 1, 0xFFFFFE00), + } + + for field_name, (value, expected_mask) in fields.items(): + prop = pymtml.c_mtmlDeviceProperty_t() + setattr(prop, field_name, value) + assert c_uint.from_buffer_copy(prop).value == expected_mask + + +def test_mpc_profile_count_stops_at_header_sentinel(): + configuration = pymtml.c_mtmlMpcConfiguration_t() + configuration.profileId[0] = 4 + configuration.profileId[1] = 1 + configuration.profileId[2] = -1 + assert configuration.profileNum == 2 + + +def test_signed_codec_session_state_preserves_unknown_value(): + assert pymtml._mtmlCodecSessionState_t(-1).value == -1 + assert pymtml.c_mtmlCodecSessionState_t(-1).value == -1 + + +def test_codec_session_state_wrapper_preserves_public_shape(monkeypatch): + def fake_get_states(_vpu, states, length): + assert length.value == 2 + states[0] = pymtml.MTML_CODEC_SESSION_STATE_IDLE + states[1] = pymtml.MTML_CODEC_SESSION_STATE_ACTIVE + return pymtml.MTML_SUCCESS + + monkeypatch.setattr( + pymtml, "_mtmlGetFunctionPointer", lambda _name: fake_get_states + ) + + states = pymtml.mtmlVpuGetEncoderSessionStates(None, 2) + assert [(state.sessionId, state.state) for state in states] == [(0, 0), (1, 1)] From 3f82c4c0e51fbcea76074ddd7010f6fa6881021e Mon Sep 17 00:00:00 2001 From: Xiaodong Ye Date: Fri, 24 Jul 2026 18:07:55 +0800 Subject: [PATCH 3/4] Bump package version to 2.4.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5144237..f6ba8b7 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ setup(name=_package_name, - version='2.2.11', + version='2.4.1', description='Python Bindings for the Moore Threads GPU Management Library', long_description=long_description, long_description_content_type='text/markdown', From 4930311a869613ed982b44a6e87a13499f17c9a9 Mon Sep 17 00:00:00 2001 From: Xiaodong Ye Date: Fri, 24 Jul 2026 18:25:47 +0800 Subject: [PATCH 4/4] Preserve compatibility aliases in friendly objects --- pymtml.py | 52 ++++++++++++++++++++++++++++++++++++++++++++-- test_abi_layout.py | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/pymtml.py b/pymtml.py index fd66c8a..aa7277c 100644 --- a/pymtml.py +++ b/pymtml.py @@ -272,8 +272,9 @@ def __str__(self): def mtmlStructToFriendlyObject(struct): d = {} - for x in struct._fields_: - key = x[0] + field_names = [field[0] for field in struct._fields_] + field_names.extend(getattr(struct, "_compat_fields_", ())) + for key in field_names: value = getattr(struct, key) # only need to convert from bytes if bytes, no need to check python version. d[key] = value.decode() if isinstance(value, bytes) else value @@ -347,6 +348,8 @@ class c_mtmlMtLinkSpec_t(_PrintableStructure): class c_mtmlPciInfo_t(_PrintableStructure): + _compat_fields_ = ("busId",) + _fields_ = [ ("sbdf", c_char * MTML_DEVICE_PCI_SBDF_BUFFER_SIZE), ("segment", c_uint), @@ -368,9 +371,19 @@ class c_mtmlPciInfo_t(_PrintableStructure): def busId(self): return self.sbdf + @busId.setter + def busId(self, value): + self.sbdf = value + ## Device property structure class c_mtmlDeviceProperty_t(_PrintableStructure): + _compat_fields_ = ( + "virtCapability", + "mpcCapability", + "mtLinkCapability", + ) + _fields_ = [ ("virtCap", c_uint, 1), ("virtRole", c_uint, 3), @@ -408,6 +421,8 @@ def mtLinkCapability(self, value): ## PCI slot info structure class c_mtmlPciSlotInfo_t(_PrintableStructure): + _compat_fields_ = ("slotType",) + _fields_ = [ ("slotId", c_uint), ("slotName", c_char * MTML_DEVICE_SLOT_NAME_BUFFER_SIZE), @@ -418,9 +433,15 @@ class c_mtmlPciSlotInfo_t(_PrintableStructure): def slotType(self): return self.slotId + @slotType.setter + def slotType(self, value): + self.slotId = value + ## Display interface spec structure class c_mtmlDispIntfSpec_t(_PrintableStructure): + _compat_fields_ = ("maxHoriRes", "maxVertRes") + _fields_ = [ ("type", c_uint), ("maxResWidth", c_uint), @@ -440,6 +461,17 @@ def maxVertRes(self): ## Virtualization type structure class c_mtmlVirtType_t(_PrintableStructure): + _compat_fields_ = ( + "deviceClass", + "apiType", + "maxResWidth", + "maxResHeight", + "memSize", + "gpuCores", + "encoderNum", + "decoderNum", + ) + _fields_ = [ ("id", c_char * MTML_VIRT_TYPE_ID_BUFFER_SIZE), ("name", c_char * MTML_VIRT_TYPE_NAME_BUFFER_SIZE), @@ -489,6 +521,8 @@ def decoderNum(self): ## Codec utilization structure class c_mtmlCodecUtil_t(_PrintableStructure): + _compat_fields_ = ("encodeUtil", "decodeUtil") + _fields_ = [ ("util", c_uint), ("period", c_uint), @@ -513,6 +547,8 @@ class c_mtmlCodecSessionState_t(c_int): ## Codec session metrics structure class c_mtmlCodecSessionMetrics_t(_PrintableStructure): + _compat_fields_ = ("sessionId", "width", "height", "fps") + _fields_ = [ ("id", c_uint), ("pid", c_uint), @@ -578,6 +614,8 @@ class c_mtmlLogCallbackConfiguration_t(_PrintableStructure): ## Log configuration structure class c_mtmlLogConfiguration_t(_PrintableStructure): + _compat_fields_ = ("filePath", "maxSize", "logLevel") + _fields_ = [ ("consoleConfig", c_mtmlLogConsoleConfiguration_t), ("systemConfig", c_mtmlLogSystemConfiguration_t), @@ -613,6 +651,8 @@ def logLevel(self, value): ## MPC profile structure class c_mtmlMpcProfile_t(_PrintableStructure): + _compat_fields_ = ("profileId", "gpuCores", "memSize") + _fields_ = [ ("id", c_uint), ("coreCount", c_uint), @@ -636,6 +676,8 @@ def memSize(self): ## MPC configuration structure class c_mtmlMpcConfiguration_t(_PrintableStructure): + _compat_fields_ = ("profileIds", "profileNum") + _fields_ = [ ("id", c_uint), ("name", c_char * MTML_MPC_CONF_NAME_BUFFER_SIZE), @@ -666,6 +708,8 @@ class c_mtmlMtLinkLayout_t(_PrintableStructure): ## Page retirement count structure class c_mtmlPageRetirementCount_t(_PrintableStructure): + _compat_fields_ = ("singleBitEcc", "doubleBitEcc") + _fields_ = [ ("sbeCount", c_uint), ("dbeCount", c_uint), @@ -682,6 +726,8 @@ def doubleBitEcc(self): ## Page retirement structure class c_mtmlPageRetirement_t(_PrintableStructure): + _compat_fields_ = ("timestamp",) + _fields_ = [ ("timestamps", c_ulonglong), ("address", c_ulonglong), @@ -694,6 +740,8 @@ def timestamp(self): class c_mtmlPageRetirementPending_t(_PrintableStructure): + _compat_fields_ = ("timestamp",) + _fields_ = [ ("cause", c_uint), ("timestamps", c_ulonglong), diff --git a/test_abi_layout.py b/test_abi_layout.py index c974864..a88b226 100644 --- a/test_abi_layout.py +++ b/test_abi_layout.py @@ -129,6 +129,47 @@ def test_pci_info_matches_header_and_exposes_bus_id_alias(): assert pci_info.busId == pci_info.sbdf +def test_compatibility_aliases_survive_friendly_conversion_and_assignment(): + pci_info = pymtml.c_mtmlPciInfo_t(busId="00000000:01:00.0") + friendly_pci_info = pymtml.nvmlStructToFriendlyObject(pci_info) + assert pci_info.sbdf == "00000000:01:00.0" + assert friendly_pci_info.busId == "00000000:01:00.0" + + slot_info = pymtml.c_mtmlPciSlotInfo_t(slotType=7) + friendly_slot_info = pymtml.mtmlStructToFriendlyObject(slot_info) + assert slot_info.slotId == 7 + assert friendly_slot_info.slotType == 7 + + profile = pymtml.c_mtmlMpcProfile_t() + profile.id = 3 + profile.coreCount = 8 + profile.memorySizeMB = 2048 + friendly_profile = pymtml.mtmlStructToFriendlyObject(profile) + assert friendly_profile.profileId == 3 + assert friendly_profile.gpuCores == 8 + assert friendly_profile.memSize == 2048 * 1024 * 1024 + + structures_with_aliases = ( + pymtml.c_mtmlPciInfo_t, + pymtml.c_mtmlDeviceProperty_t, + pymtml.c_mtmlPciSlotInfo_t, + pymtml.c_mtmlDispIntfSpec_t, + pymtml.c_mtmlVirtType_t, + pymtml.c_mtmlCodecUtil_t, + pymtml.c_mtmlCodecSessionMetrics_t, + pymtml.c_mtmlLogConfiguration_t, + pymtml.c_mtmlMpcProfile_t, + pymtml.c_mtmlMpcConfiguration_t, + pymtml.c_mtmlPageRetirementCount_t, + pymtml.c_mtmlPageRetirement_t, + pymtml.c_mtmlPageRetirementPending_t, + ) + for structure_type in structures_with_aliases: + structure = structure_type() + friendly = pymtml.mtmlStructToFriendlyObject(structure) + assert all(hasattr(friendly, name) for name in structure._compat_fields_) + + def test_compatibility_aliases_use_header_backing_fields(): prop = pymtml.c_mtmlDeviceProperty_t() prop.virtCapability = 1