🔧 模块二:MUSA开发环境搭建

pymtml设备管理与监控

入门·⏱ 45分钟·📅 2026-08-10

一、什么是pymtml?

pymtml(Python Moore Threads Management Library)是摩尔线程官方提供的 GPU 设备管理 Python 库,由发行包名 mthreads-ml-py 提供。它让开发者可以用几行 Python 代码完成 GPU 的枚举、状态查询、温度监控、显存使用率统计、拓扑查询等工作,是构建 监控面板、调度脚本、推理服务健康检查 的基础工具。

如果你熟悉 NVIDIA 生态中的 pynvml,那么 pymtml 的 API 风格会让你感到非常亲切 —— 摩尔线程在设计时有意对齐了命名习惯与调用流程,让 CUDA 生态的运维脚本可以低成本迁移到 MUSA 环境。

💡
pymtml 的典型使用场景
1. 训练 / 推理服务监控:周期性采集 GPU 显存、温度、利用率,推送到 Prometheus / Grafana。
2. 作业调度:根据空闲显存和温度挑选目标 GPU,自动设置 MUSA_VISIBLE_DEVICES。
3. 健康检查:为 vLLM-MUSA / SGLang-MUSA 等推理服务提供 readiness probe。
4. 从 CUDA 迁移:把原本依赖 pynvml 的运维脚本一一对应改为 pymtml。

二、安装

pymtml 以 mthreads-ml-py 的名字发布到摩尔线程的 Python 索引中,安装完成后在代码里 import pymtml 即可。要正确工作,你的系统需要已经安装好 MUSA 驱动,推荐配合 MUSA SDK >= 4.3.2 使用。

bash
# 安装
pip install mthreads-ml-py

# 如果你的环境使用摩尔线程私有索引
pip install mthreads-ml-py --index-url https://developer.mthreads.com/simple

# 验证:应看到类似 pymtml x.y.z 的版本号
python -c "import pymtml; print(pymtml.__version__)"
⚠️
驱动版本要求
pymtml 调用的是底层 MTML 接口,因此 必须先安装与当前内核兼容的摩尔线程驱动。如果 mtmlInit() 报错,大概率是驱动缺失或版本过低,先用 mthreads-gmi 命令确认驱动就绪。

三、第一次设备查询

最常见的开场白:列出本机所有 MTT GPU,逐一打印名称、显存、温度。

python
import pymtml

# 初始化 MTML 库(每个进程调用一次)
pymtml.mtmlInit()

try:
    count = pymtml.mtmlDeviceGetCount()
    print(f"检测到 {count} 张摩尔线程 GPU\n")

    for i in range(count):
        handle = pymtml.mtmlDeviceGetHandleByIndex(i)
        name = pymtml.mtmlDeviceGetName(handle)
        uuid = pymtml.mtmlDeviceGetUUID(handle)

        # 显存信息(单位:字节)
        mem = pymtml.mtmlDeviceGetMemoryInfo(handle)
        used_gb = mem.used / 1024**3
        total_gb = mem.total / 1024**3

        # 温度(sensor 0 通常是 die 温度,单位 °C)
        temp = pymtml.mtmlDeviceGetTemperature(handle, 0)

        # 功耗(单位 mW)
        power_mw = pymtml.mtmlDeviceGetPowerUsage(handle)

        # 利用率(0~100)
        util = pymtml.mtmlDeviceGetUtilizationRates(handle)

        print(f"GPU {i}: {name}")
        print(f"  UUID:   {uuid}")
        print(f"  显存:   {used_gb:.2f} / {total_gb:.2f} GiB")
        print(f"  温度:   {temp} °C")
        print(f"  功耗:   {power_mw/1000:.1f} W")
        print(f"  使用率: GPU {util.gpu}%  显存 {util.memory}%\n")

finally:
    pymtml.mtmlShutdown()

关键点:

  • mtmlInit() / mtmlShutdown() 必须成对调用,建议用 try/finally 包裹。
  • mtmlDeviceGetHandleByIndex(i) 返回一个句柄,后续所有查询都基于它。
  • mem.used、mem.total、mem.free 单位都是 字节。

四、查询 GPU 拓扑与 PCIe 信息

多卡场景下,显存不够时你通常希望把通信密集的任务放在 拓扑距离近 的 GPU 上(例如共享同一个 PCIe Switch)。pymtml 提供 PCIe 信息查询接口:

python
pymtml.mtmlInit()

count = pymtml.mtmlDeviceGetCount()
for i in range(count):
    h = pymtml.mtmlDeviceGetHandleByIndex(i)
    pci = pymtml.mtmlDeviceGetPciInfo(h)
    print(f"GPU {i}: PCI bus {pci.bus:02x}:{pci.device:02x}.{pci.function} "
          f"(domain {pci.domain:04x})  Device ID: {pci.pciDeviceId:#x}")

pymtml.mtmlShutdown()

如果你的 GPU 支持 PCIe Gen5 或者 MT-Link 互联,后续可以通过 mtmlDeviceGetTopologyCommonAncestor 类接口判断两张卡的共同祖先节点 —— 这是实现 多卡亲和性调度 的基础。

五、构建一个简单的监控脚本

下面的脚本每 2 秒采样一次,把指标打印成表格,可以直接 Ctrl+C 退出。稍加改造即可接入 Prometheus textfile collector 或自建的时序数据库。

python
import time
import pymtml
from datetime import datetime

def snapshot():
    rows = []
    count = pymtml.mtmlDeviceGetCount()
    for i in range(count):
        h = pymtml.mtmlDeviceGetHandleByIndex(i)
        name = pymtml.mtmlDeviceGetName(h)
        mem = pymtml.mtmlDeviceGetMemoryInfo(h)
        util = pymtml.mtmlDeviceGetUtilizationRates(h)
        temp = pymtml.mtmlDeviceGetTemperature(h, 0)
        power = pymtml.mtmlDeviceGetPowerUsage(h) / 1000.0
        rows.append({
            "idx": i,
            "name": name,
            "mem_used_gb": round(mem.used / 1024**3, 2),
            "mem_total_gb": round(mem.total / 1024**3, 2),
            "gpu_util": util.gpu,
            "mem_util": util.memory,
            "temp_c": temp,
            "power_w": round(power, 1),
        })
    return rows

if __name__ == "__main__":
    pymtml.mtmlInit()
    try:
        while True:
            ts = datetime.now().strftime("%H:%M:%S")
            print(f"\n=== {ts} ===")
            print(f"{'idx':<4}{'name':<20}{'mem(GiB)':<18}{'GPU%':<6}{'MEM%':<6}{'T°C':<6}{'W':<6}")
            for r in snapshot():
                print(f"{r['idx']:<4}{r['name']:<20}"
                      f"{r['mem_used_gb']}/{r['mem_total_gb']:<12}"
                      f"{r['gpu_util']:<6}{r['mem_util']:<6}{r['temp_c']:<6}{r['power_w']:<6}")
            time.sleep(2)
    except KeyboardInterrupt:
        pass
    finally:
        pymtml.mtmlShutdown()

六、与 vLLM-MUSA / SGLang-MUSA 配合

在部署 LLM 推理服务时,pymtml 扮演两个关键角色:

1
启动前选卡:扫描所有 GPU,挑出空闲显存足够、温度较低的卡,把索引拼进 MUSA_VISIBLE_DEVICES。
2
运行时健康探针:定期查询显存与温度,当温度超过阈值或利用率长期为 0 时触发告警 / 重启。
python
import os, pymtml

def pick_best_gpu(min_free_gb=40, max_temp=85):
    pymtml.mtmlInit()
    try:
        best, best_free = -1, 0
        for i in range(pymtml.mtmlDeviceGetCount()):
            h = pymtml.mtmlDeviceGetHandleByIndex(i)
            mem = pymtml.mtmlDeviceGetMemoryInfo(h)
            temp = pymtml.mtmlDeviceGetTemperature(h, 0)
            free_gb = mem.free / 1024**3
            if free_gb >= min_free_gb and temp <= max_temp and free_gb > best_free:
                best, best_free = i, free_gb
        return best
    finally:
        pymtml.mtmlShutdown()

idx = pick_best_gpu()
if idx < 0:
    raise RuntimeError("没有满足条件的 GPU")

os.environ["MUSA_VISIBLE_DEVICES"] = str(idx)
print(f"已选中 GPU {idx} 用于 vLLM-MUSA 服务")
# 接下来启动 vllm serve …

七、从 pynvml 迁移到 pymtml

对于已有的 NVIDIA 运维脚本,迁移成本非常低。下表给出最常用 API 的一一对应关系:

pynvml (CUDA)pymtml (MUSA)说明
nvmlInitmtmlInit初始化
nvmlShutdownmtmlShutdown释放
nvmlDeviceGetCountmtmlDeviceGetCount设备数
nvmlDeviceGetHandleByIndexmtmlDeviceGetHandleByIndex句柄
nvmlDeviceGetMemoryInfomtmlDeviceGetMemoryInfo显存 total/free/used
nvmlDeviceGetUtilizationRatesmtmlDeviceGetUtilizationRatesGPU/显存利用率
nvmlDeviceGetTemperaturemtmlDeviceGetTemperature温度
nvmlDeviceGetPowerUsagemtmlDeviceGetPowerUsage功耗 mW
CUDA_VISIBLE_DEVICESMUSA_VISIBLE_DEVICES环境变量
表1:pynvml → pymtml API 对照
✅
本课小结
pymtml 是摩尔线程官方的 Python GPU 管理库,发行包名为 mthreads-ml-py。通过与 pynvml 对齐的 API 设计,它让 GPU 的枚举、显存、温度、功耗、拓扑查询都变得像调用 torch.cuda.* 一样自然。在构建推理服务的健康检查、训练作业的调度脚本、机房监控面板时,pymtml 都是首选工具。参考:摩尔线程开发者站。

附录:2026-08-10 更新章节 —— MTML 2.4.1 支持与 GPU 监控实战

🆕
本章节新增于 2026-08-10
本章节合并两条线索:① mthreads-ml-py PR #13 增加了 MTML 2.4.1 支持与 legacy runtime 回退,Python 包版本升至 2.4.1;② 官方教程仓库 tutorial_on_musa PR #65 新增了 GPU monitor 内容。说明:PR #65 的公开描述仅注明"基于 #63",缺少可引用的技术正文,因此本站不为其单独开课,而是把可确认的依赖变化与完整可运行的监控实践并入本课作为更新章节。下文中凡涉及 MTML 版本与加载顺序的事实均来自 PR #13;监控脚本本身为基于 pymtml 公开 API 的通用实现。

1. MTML 2.4.1 的加载顺序变化

PR #13 的核心是一处看似很小、实则关系到升级平滑性的改动:优先加载 libmtml.so.2,把 libmtml.so 作为回退。

为什么要这样做?共享库的版本化命名(.so.2)表示 ABI 主版本,而无版本号的 libmtml.so 通常是指向当前版本的开发用符号链接。老实现直接加载 libmtml.so,意味着一旦系统上装了多个版本、或者符号链接指向了不期望的版本,Python 侧就会加载到 ABI 不匹配的库——表现为诡异的结构体字段错位,而不是干脆的加载失败。新实现优先绑定明确的 ABI 主版本,同时保留回退路径,兼顾了新旧环境。

与此配套,PR #13 让 ctypes 结构对齐 MTML 2.x 的 C ABI,并保留了兼容别名——这意味着你按本课正文写的旧脚本不需要改就能继续工作。官方在 MTT S80 上对 MTML 2.4.1 与 2.2.0 都做过冒烟验证,说明这次升级是双向兼容的。

项目变化前变化后(2.4.1)
库加载顺序直接加载 libmtml.so优先 libmtml.so.2,回退 libmtml.so
ctypes 结构对齐旧 ABI对齐 MTML 2.x C ABI,保留兼容别名
Python 包版本2.2.x2.4.1
验证范围—S80 上对 2.4.1 与 2.2.0 双版本冒烟
⚠️
这不只是监控脚本的事
mthreads-ml-py 已经成为推理栈的硬依赖:vllm-musa PR #149 明确要求 mthreads-ml-py>=2.4.1。也就是说,如果你的推理环境里 pymtml 版本过低,受影响的可能不只是监控面板,还包括 vLLM-MUSA 自身的启动与设备发现。升级 vLLM-MUSA 前请先核对这个依赖。同一 PR 还把 MUSA wheel 名称去掉了 s5000 后缀,详见 m5-l3 的 2026-08-10 更新章节。

2. Step-by-Step:升级与版本核对

# Step 1 — 查看当前版本与实际加载的动态库
pip show mthreads-ml-py | grep -E "Version|Location"
python - <<'PY'
import ctypes.util, subprocess, sys
for name in ("libmtml.so.2", "libmtml.so"):
    try:
        ctypes.CDLL(name)
        print("loadable:", name)
    except OSError as e:
        print("not loadable:", name, "-", e)
PY

# Step 2 — 升级到 2.4.1(vllm-musa 的最低要求)
pip install "mthreads-ml-py>=2.4.1"

# Step 3 — 冒烟验证
python - <<'PY'
import pymtml
pymtml.mtmlLibraryInit()
n = pymtml.mtmlLibraryCountDevice()
print("device count:", n)
pymtml.mtmlLibraryShutDown()
PY

# Step 4 — 交叉核对 vllm-musa 依赖是否满足
pip check 2>&1 | grep -i mthreads || echo "dependency ok"

3. 完整监控示例:一个可直接投产的采集器

下面这个脚本把本课正文讲过的各项查询组织成一个可循环采集、可输出 CSV、可作为告警数据源的完整工具。它覆盖设备枚举、显存、利用率、温度、功耗四类核心指标,并做了健壮的异常处理——监控脚本挂掉比没有监控更糟,因为它会给你"一切正常"的错觉。

#!/usr/bin/env python3
"""MTT GPU 监控采集器 — 基于 pymtml (mthreads-ml-py >= 2.4.1)
用法:python gpu_monitor.py --interval 5 --csv /var/log/mtgpu.csv
"""
import argparse, csv, os, signal, sys, time
from datetime import datetime

import pymtml

RUNNING = True

def _stop(signum, frame):
    global RUNNING
    RUNNING = False

def safe(fn, default=None):
    """任何单项指标失败都不应该让整个采集器退出。"""
    try:
        return fn()
    except Exception:
        return default

def collect():
    rows = []
    count = pymtml.mtmlLibraryCountDevice()
    for i in range(count):
        dev = pymtml.mtmlLibraryInitDeviceByIndex(i)
        gpu = safe(lambda: pymtml.mtmlDeviceInitGpu(dev))
        rows.append({
            "ts":        datetime.now().isoformat(timespec="seconds"),
            "index":     i,
            "name":      safe(lambda: pymtml.mtmlDeviceGetName(dev), "unknown"),
            "uuid":      safe(lambda: pymtml.mtmlDeviceGetUUID(dev), "unknown"),
            "mem_total": safe(lambda: pymtml.mtmlMemoryGetTotal(
                             pymtml.mtmlDeviceInitMemory(dev)), 0),
            "mem_used":  safe(lambda: pymtml.mtmlMemoryGetUsed(
                             pymtml.mtmlDeviceInitMemory(dev)), 0),
            "util":      safe(lambda: pymtml.mtmlGpuGetUtilization(gpu), -1),
            "temp":      safe(lambda: pymtml.mtmlGpuGetTemperature(gpu), -1),
            "power":     safe(lambda: pymtml.mtmlDeviceGetPowerUsage(dev), -1),
        })
    return rows

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--interval", type=float, default=5.0)
    ap.add_argument("--csv", default="")
    ap.add_argument("--temp-warn", type=float, default=85.0)
    args = ap.parse_args()

    signal.signal(signal.SIGINT, _stop)
    signal.signal(signal.SIGTERM, _stop)

    pymtml.mtmlLibraryInit()
    writer, fh = None, None
    if args.csv:
        new = not os.path.exists(args.csv)
        fh = open(args.csv, "a", newline="")
        writer = csv.DictWriter(fh, fieldnames=list(collect()[0].keys()))
        if new:
            writer.writeheader()

    try:
        while RUNNING:
            for r in collect():
                used_gb  = r["mem_used"]  / 1024 if r["mem_used"]  else 0
                total_gb = r["mem_total"] / 1024 if r["mem_total"] else 0
                print("[%s] GPU%d %-16s util=%3s%% mem=%.1f/%.1fGB temp=%s C power=%sW"
                      % (r["ts"], r["index"], r["name"], r["util"],
                         used_gb, total_gb, r["temp"], r["power"]))
                if isinstance(r["temp"], (int, float)) and r["temp"] >= args.temp_warn:
                    print("  !! WARN: GPU%d temperature %s C >= %s C"
                          % (r["index"], r["temp"], args.temp_warn), file=sys.stderr)
                if writer:
                    writer.writerow(r)
            if fh:
                fh.flush()
            time.sleep(args.interval)
    finally:
        if fh:
            fh.close()
        pymtml.mtmlLibraryShutDown()
        print("monitor stopped cleanly")

if __name__ == "__main__":
    main()
💡
三个让监控真正可用的设计要点
① 单项失败不致命:safe() 包装让某个指标查询失败时返回默认值而非抛出,避免一块卡异常拖垮整个采集。② 信号处理与资源释放:捕获 SIGINT/SIGTERM 并在 finally 里调用 mtmlLibraryShutDown(),容器优雅停机时不会泄漏句柄。③ 落盘与 flush:CSV 每轮 flush,进程被强杀时也不会丢掉最后一批数据。这三点看着琐碎,但正是"能跑"与"能长期跑"之间的差别。

4. 监控指标该看什么:从数据到决策

采集到数据只是第一步,真正难的是知道哪个指标异常时应该做什么。下面把采集器的五类指标逐一对应到实际的运维动作。

显存使用率是推理服务最关键的容量指标。对 vLLM/SGLang 这类预分配显存的引擎,启动后显存占用会迅速撑到设定值并保持平稳,因此显存曲线突然波动反而是异常信号,可能意味着服务重启过。反之,对训练作业,显存周期性波动是正常的,但峰值逐步抬升则提示可能有泄露。

利用率需要配合业务负载一起看。一个常见的误判是把低利用率直接归因为"GPU 没跑满,可以多接量"。实际上 Decode 阶段天然就是访存受限的,利用率偷低很正常;真正要关心的是利用率与吞吐是否同时低——那才说明服务卡在了别处(比如 CPU 预处理、网络、或者上一课讲的同步点)。

温度与功耗是硬件健康信号,也是性能稳定性的隐形变量。当一个节点上某张卡的温度明显高于其他卡时,不要只看绝对值是否超阈,卡间差异本身就是信号——它可能意味着风道阻塞、散热库老化,或者多卡任务负载不均。多卡训练场景下,一张卡因过热降频会拖慢整个集合通信,表现为全局吞吐下降而非单卡变慢,极难定位。这就是为什么采集器要按卡逐个记录而不是只存节点均值。

设备枚举与 UUID看似只是元数据,实际上是排障时最有用的一栏。容器化环境下,GPU 的逻辑编号会因为透传顺序不同而变化,只记录 index 会导致日志与物理卡对不上号。把 UUID 一并落盘,是跨容器、跨重启追踪同一张物理卡的唯一可靠方式。

5. 部署为常驻服务与排障

# 以 systemd 常驻(示例单元文件)
cat > /etc/systemd/system/mtgpu-monitor.service <<'EOF'
[Unit]
Description=MTT GPU Monitor
After=network.target

[Service]
ExecStart=/usr/bin/python3 /opt/monitor/gpu_monitor.py --interval 5 --csv /var/log/mtgpu.csv
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now mtgpu-monitor
systemctl status mtgpu-monitor --no-pager

# 快速查看最近记录
tail -5 /var/log/mtgpu.csv
现象根因处理
OSError: libmtml.so not found驱动未安装或库路径缺失安装 MT 驱动,检查 LD_LIBRARY_PATH
结构体字段值异常离谱ABI 不匹配(加载到旧版库)升级到 mthreads-ml-py>=2.4.1,确认加载 libmtml.so.2
容器内查不到设备未透传 GPU 设备节点启动时增加 --device=/dev/mtgpu0 等
vLLM-MUSA 启动报 pymtml 相关错误版本低于 2.4.1按 PR #149 要求升级

回滚:pip install "mthreads-ml-py==2.2.0" --force-reinstall 可退回旧版本,由于 PR #13 保留了兼容别名,回滚不会破坏按旧 API 编写的脚本;但请注意,若同机运行 vLLM-MUSA 新版本,回滚会违反其 >=2.4.1 的依赖要求。

本章节小结:MTML 2.4.1 的核心变化是库加载顺序改为优先 libmtml.so.2、回退 libmtml.so,ctypes 结构对齐 MTML 2.x C ABI 并保留兼容别名,Python 包版本升至 2.4.1,官方在 S80 上对 2.4.1 与 2.2.0 做过双版本冒烟(PR #13)。它同时是 vLLM-MUSA 的硬依赖下限(PR #149),因此升级推理栈前必须先核对。官方教程仓库的 GPU monitor 更新(tutorial_on_musa PR #65)因公开描述不足以支撑独立课程,本站以本章节的完整采集器实现替代,覆盖枚举、显存、利用率、温度、功耗五类指标与 systemd 常驻部署。