跳转至内容
  • 版块
  • 最新
  • 标签
  • 热门
  • 用户
  • 群组
皮肤
  • 浅色
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • 深色
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • 默认(LCZ-Blue)
  • 不使用皮肤
  • LCZ-Green
  • LCZ-Blue
  • LCZ-Black
折叠
品牌标识

抡锤者

首页 版块 标签
硬件
装备秀
AI 广场 App
  1. 主页
  2. 版块
  3. AI Agent
  4. 给 DGX Spark 写了个零依赖实时监控面板:1 个 Python 文件 + 1 个 HTML 文件

给 DGX Spark 写了个零依赖实时监控面板:1 个 Python 文件 + 1 个 HTML 文件

已定时 已固定 已锁定 已移动 优秀 AI Agent
dgxspark本地模型
6 帖子 3 发布者 220 浏览
  • 从旧到新
  • 从新到旧
  • 最多赞同
回复
  • 在新帖中回复
登录后回复
此主题已被删除。只有拥有主题管理权限的用户可以查看。
  • J
    J
    juntucoo
    体面乡绅
    编写于 最后由 编辑
    #1

    给 DGX Spark 写了个零依赖实时监控面板:1 个 Python 文件 + 1 个 HTML 文件

    起因:在 DGX Spark 上跑大模型时,想随时看清 CPU / GPU / 统一内存 / 磁盘 / 网络 / 推理吞吐的真实状态,
    以及推理引擎(推理服务)到底跑得怎么样。官方 Dashboard 看不了推理细节,nvtop/btop 又看不到
    LLM 侧的指标,索性自己写了一个。

    约束:零第三方依赖——后端只用 Python 标准库,前端单文件 HTML + 原生 JS,不引任何 CDN。
    拷过去 python3 monitor.py 就能跑。

    [整体效果]
    01-overview.png


    目录

    • 它有什么
    • 架构
    • 值得抄的 6 个实现细节
    • 部署
    • 踩坑清单
    • 文件清单
    • 已知限制

    它有什么

    17 个面板,全部可拖动、可缩放、可换位置,布局存在浏览器本地,导出一段 JSON 就能在别的机器上复现。

    顶部 9 张 KPI 卡带迷你走势线;中间 6 张曲线图支持鼠标悬停出十字线和数值气泡,Y 轴单位会在
    B/s → KB/s → MB/s 之间自动切换。

    [LLM 运维区]
    02-llm-ops.png

    上排是 推理引擎(running/waiting、解码速度、TTFT、TPOT、KV Cache 分池占用、MTP 接受率)、
    容器 llm(CPU / 内存 / net / block IO)、一键测速(发一次真实对话,按 usage 计数算吞吐)。

    下排是硬件明细:CPU 每核负载(20 核 GB10)、温度传感器 / 风扇(带过热状态染色)、
    磁盘 I/O、网络接口(含错误/丢包计数)。

    [硬件表格区]
    03-tables.png

    再往下是 进程 Top、引擎指标明细表、当前生效配置(直接读容器里的环境变量,一眼看到
    TF_SOLO_ROWS / TF_SPILL_GIB 这些实际生效的参数)、容器日志尾部。

    [进程 / 引擎指标 / 生效配置]
    04-detail.png

    交互细节

    鼠标悬停在曲线上,十字线定位到具体时刻,气泡给出该时刻的各序列数值:

    [悬停交互]
    05-hover.png

    拖卡片标题栏换位置,拖右下角的小三角改大小。布局可以导出成 JSON:

    [布局导出]
    06-layout.png

    复制这段 JSON,粘到另一台机器上点「应用」,就是一模一样的排布。


    架构

    monitor.py    ~34 KB   Python 3 标准库,无第三方依赖
      ├─ 采样线程   1 Hz 写环形缓冲(deque(maxlen=...))
      ├─ HTTP 服务  ThreadingHTTPServer
      └─ API
          /api/snapshot   当前快照(含 host/cpu/memory/gpu/sensors/disk/network/processes/engine)
          /api/series     历史序列,?window=60|300|900|1800|3600
          /api/stats      snapshot + 容器 stats + uptime
          /api/config     容器镜像/状态/环境变量(docker inspect)
          /api/logs       容器日志尾部 ?tail=200
          /api/probe      发一次真实对话,测 TTFT / decode 速度
          /api/key        设置 API Key
          /api/healthz
    
    index.html    ~49 KB   单文件:HTML + CSS + JS,无 CDN
    

    为什么服务端采样? 因为磁盘/网络的速率本质上就是「两次采样之间的差值」,没法在 HTTP 请求发生的
    那一刻凭空算出来。所以有一个后台线程固定 1 Hz 采样写进 deque,前端只是来取。


    值得抄的 6 个实现细节

    1. 速率只能用 delta 算,不能在请求时重新采样

    这是最容易踩的坑。直觉写法是「收到请求 → 读一次 /proc/diskstats → 和上次比」,但**「上次」存在哪**
    是问题:如果请求比采样线程更频繁,就会拿两个几乎同时刻的采样点作差,差值被摊薄成一个极小的时间窗,
    算出来的速率要么是 0,要么抖得没法看。

    正确做法是:后台线程独占采样,HTTP 只返回缓存的结果。

    def disk_info(prev, dt):
        """prev 是上一次采样的 /proc/diskstats 快照,dt 是两次采样之间的时间差。"""
        for name, v in cur.items():
            old = (prev or {}).get(name)
            if old and dt > 0:
                d["read_bps"]  = round((v[2] - old[2]) * 512 / dt, 1)   # 扇区数 × 512 = 字节
                d["write_bps"] = round((v[6] - old[6]) * 512 / dt, 1)
                d["read_iops"] = round((v[0] - old[0]) / dt, 2)
    

    后台循环里 dt 用真实经过时间:

    def sample_once():
        now = time.time()
        dt = max(0.5, now - STATE["prev"].get("ts", now - SAMPLE_INTERVAL))
        STATE["prev"]["ts"] = now
    

    2. 统一内存架构下,nvidia-smi 的显存字段是 [N/A]

    GB10 的 CPU 和 GPU 共享同一块内存(标称 128 GB,系统可见约 122 GiB),
    nvidia-smi 对显存总量/占用直接返回 [N/A]:

    $ nvidia-smi --query-gpu=memory.total,memory.used --format=csv
    [N/A], [N/A]
    

    所以面板里的「统一内存」读的是 /proc/meminfo,而不是显存字段;GPU 面板只取利用率/温度/功耗。
    另外 power.limit、fan.speed 在这台机器上也是 [N/A],别指望它们。

    顺带一个有用的取法:逐进程显存是可以拿到的(--query-compute-apps),把它求和就得到
    「模型常驻显存」:

    rc, so, _se = run(["nvidia-smi", "--query-compute-apps=pid,used_gpu_memory,process_name",
                       "--format=csv,noheader,nounits"])
    out["model_gpu_mib"] = round(sum(p["mem_mib"] for p in procs), 1)
    

    3. 用 12 栅格显式定位,而不是 auto-fit

    最初用 grid-template-columns: repeat(auto-fit, minmax(330px, 1fr)),结果卡片宽度随窗口变化,
    而且没法留空位、没法叠放。改成显式坐标后什么都解决了:

    .grid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));
          grid-auto-rows:44px;gap:10px}
    .card{grid-column:span var(--w,3);grid-row:span var(--h,5)}
    

    每张卡片存 {x, y, w, h}(单位是栅格格数),JS 直接写:

    el.style.gridColumn = (c.x + 1) + " / span " + c.w;
    el.style.gridRow    = (c.y + 1) + " / span " + c.h;
    

    显式坐标才能表达「第二行右边留空」和「磁盘 I/O 叠在网络接口下面」这种排布——自动排布是做不到的。

    4. 卡片被压住时「就近挪开」,而不是跳到页面另一头

    拖动时如果落点压住了别的卡片,需要把被压的挪走。朴素的实现是「从左上角扫描找第一个空位」,
    结果卡片会莫名其妙飞到页面最上面。这里改成收集所有空位,再按离原位的距离排序:

    function findSpot(w, h, exceptId, prefer){
      const free = [];
      for (let y = 0; y < 400; y++)
        for (let x = 0; x <= GRID.cols - w; x++){
          const cand = {x, y, w, h};
          let ok = true;
          for (const [id, c] of Object.entries(state)){
            if (id === exceptId) continue;
            if (overlaps(cand, c)){ ok = false; break; }
          }
          if (ok) free.push({x, y});
        }
      if (!free.length) return {x: 0, y: 0};
      if (prefer)   // 就近安置:离原位越近越优先
        free.sort((a, b) => (Math.abs(a.x - prefer.x) + Math.abs(a.y - prefer.y))
                          - (Math.abs(b.x - prefer.x) + Math.abs(b.y - prefer.y)));
      return free[0];
    }
    

    5. canvas 曲线:nice tick + 自动单位 + 悬停十字线

    不用图表库,纯 canvas 画。三点值得注意:

    Y 轴刻度取「好看」的步长,否则会出现 7.4993 这种刻度:

    niceStep(range){
      const raw = range / 4, mag = Math.pow(10, Math.floor(Math.log10(raw) || 0));
      const n = raw / mag;
      return (n <= 1 ? 1 : n <= 2 ? 2 : n <= 2.5 ? 2.5 : n <= 5 ? 5 : 10) * mag;
    }
    

    单位自动切换:网络速率可能是 500 B/s 也可能是 200 MB/s,固定单位必然有一头显示成
    0.00 MB/s 或 123456789 B/s。所以按量级挑单位:

    if (this.opts.autoUnit){
      const m = Math.max(Math.abs(vmin), Math.abs(vmax));
      if      (m >= 1048576){ this._scale = 1048576; this._unit = "MB/s"; }
      else if (m >= 1024)   { this._scale = 1024;    this._unit = "KB/s"; }
      else                 { this._scale = 1;       this._unit = "B/s"; }
    }
    

    悬停十字线:鼠标位置换算成时间,吸附到最近的采样点:

    onMove(e){
      const g = this._geo;
      const x = e.clientX - this.cv.getBoundingClientRect().left;
      this.hover = (x - g.padL) / g.pw;
      this.draw();                      // 重画时画十字线
      const t = g.t0 + (g.t1 - g.t0) * this.hover;
      for (const s of this.series){
        const p = this.nearest(s.data, t);   // 线性找最近点
        ...
      }
    }
    

    6. 布局持久化 + JSON 导出/导入

    位置存 localStorage,再提供导出/导入,这样「我摆好的版面」可以固化成一段文本:

    const LAYOUT_KEY = "sparkmon_layout_v3";
    function readLayout(){ return {layout: JSON.parse(JSON.stringify(state)), win: WIN}; }
    function saveLayout(){ localStorage.setItem(LAYOUT_KEY, JSON.stringify(readLayout())); }
    

    导出的 JSON 长这样,一眼就能看出每张卡片的格子坐标:

    {
      "layout": {
        "card-cpu":    { "x": 0,  "y": 0,  "w": 2, "h": 5 },
        "card-engine": { "x": 0,  "y": 5,  "w": 6, "h": 5 },
        "card-logs":   { "x": 0,  "y": 24, "w": 12, "h": 6 }
      },
      "win": 300
    }
    

    部署

    # 1. 放到机器上
    scp -r dgx-monitor <user>@<SPARK_IP>:~/
    
    # 2. 一键安装(注册 systemd 服务 + 自检)
    cd dgx-monitor && bash install.sh
    

    install.sh 做的事:把文件装到 ~/dgx/monitor/、用 sed 替换 systemd 单元里的
    User= / WorkingDirectory= / MONITOR_PORT=、daemon-reload、enable --now,
    最后 curl 一下首页确认真的起来了。

    自检脚本检查这几项,任何一项不过会明确指出:

    $ bash verify.sh
      PASS  服务 spark-monitor active
      PASS  首页 200(48958 字节)
      PASS  API /api/stats 200
      PASS  新版前端已生效(找到栅格卡片)
      PASS  快照字段齐全(gpu/sensors/disk/network)
      PASS  /api/config 200
      PASS  /api/logs 200
      PASS  /api/healthz 200
      PASS  llm      :8888 /health ok
    

    也可以不用 systemd,直接前台跑:

    MONITOR_PORT=8890 python3 monitor.py
    

    踩坑清单(实战)

    这几条都是真踩过的,写出来省得别人再踩一遍。

    1. 一个没定义的函数,能让半张页面变成空白。
    前端调了 setBar() 但从没定义过它,ReferenceError 直接把 loadSnapshot() 从中间打断,
    结果 GPU、传感器、磁盘、网络、进程、容器、引擎表全部没渲染,而页面看起来只是「没数据」。
    教训:前端的渲染要分段,别让一个异常连坐后面所有模块;排查时先看 console 报错。

    2. 同名函数声明会互相覆盖。
    布局管理里有个 function render() 画栅格,数据渲染里也有 function render(s) 画面板——
    JS 里函数声明会被提升,后定义的那个赢,于是数据渲染被静默顶掉,页面全空但不报错。
    后来把布局那个改名 paintLayout() 才解决。

    3. 轮询打爆后端日志。
    监控自己每秒去拉推理引擎的 /metrics,但没带 API Key,结果后端日志里刷了 1000 多条 401。
    加了 API Key 还不够,又加了失败退避:

    if resp.status == 401:
        self._engine_blocked_until = now + 300.0    # 401 就安静 5 分钟
    

    4. 单位约定不一致,数值会离谱 1000 倍。
    Prometheus 的 time_to_first_token_seconds 是秒,但另一套采集里换算成了毫秒,
    两边混用就会看到「TTFT 均值 105952.685 s」。贴指标前先确认单位,表头也要写清楚。

    5. Y 轴标签被裁掉。
    padL: 48 放不下 87.81 MB/s,首字符被切掉变成 7.81 MB/s,看起来像数据错了。
    标签宽度要按「最长刻度 + 单位」算。

    6. install.sh 用 enable --now 不会重启已在跑的服务。
    enable --now 对已运行的服务是 no-op,改完代码重新安装,线上跑的还是老版本。
    正确做法是 systemctl --user enable 之后显式 restart。

    7. systemd 单元里的 User= 是写死的。
    别人拿去用就会因为用户名不同而起不来。安装脚本里用 sed 替换成当前用户。


    文件清单

    dgx-monitor/
    ├── monitor.py              后端(标准库,零依赖)      ~34 KB
    ├── index.html              前端(单文件,无 CDN)      ~49 KB
    ├── spark-monitor.service   systemd 单元模板
    ├── install.sh              一键安装
    ├── verify.sh               部署后自检
    └── layout.test.js          布局引擎单元测试
    

    layout.test.js 是给布局逻辑写的单元测试(纯逻辑,Node 里跑,浏览器无关),
    校验默认版面不重叠、关键排位、碰撞推挤、就近安置、持久化往返:

    $ node layout.test.js
    == 1. 默认版面(17 张卡片)自身不重叠 ==
      PASS  全部卡片互不重叠
      PASS  状态里有 17/17 张卡片
    ...
    == 5. findSpot 返回真正空的位子 ==
      PASS  空位 {"x":0,"y":30} 无冲突
      PASS  带 prefer 取位不更远:{"x":8,"y":30}(距离 21 ≤ 扫描序 29)
    全部通过 ✅
    

    怎么拿到源码

    源码就上面这 7 个文件,没有构建步骤、没有依赖清单,全量 84 KB。几种给法:

    • 贴到 gist / 仓库:只有 7 个文件,直接建个仓库传上去最省事
    • 打包成 tar.gz:tar czf dgx-monitor.tar.gz dgx-monitor/,随帖附上
    • 从帖子直接抄:本文已经把最关键的 6 段实现贴出来了,照着写也能复现个七七八八

    如果你想给别人的是「换个样式 / 换数据源」的版本,改 index.html 里的 CHARTS / DEFAULT_LAYOUT
    两个常量就够了,数据源在 monitor.py 的 sample_once() 里。

    源码里的占位符

    发布版把和具体环境绑定的部分换成了占位符,拿到后改这几处即可:

    占位符 在哪 改成
    <ENGINE_PREFIX> monitor.py(Prometheus 指标前缀,约 20 处) 你引擎的前缀,例如 vllm:
    <model-name> monitor.py 模型名;留空则自动从 /v1/models 发现
    <user> spark-monitor.service 不用手改,install.sh 会自动替换成当前用户名

    已知限制

    • 面板不绑定任何推理后端。引擎相关的部分靠读 /metrics(推理服务 / vLLM 都行),
      没起容器时那几块是空的,其余面板照常工作。
    • API Key 只存在于内存或容器环境变量里,面板本身不落盘、不外传。
    • 布局存在浏览器 localStorage,换浏览器要靠「布局…」里的 JSON 搬运。
    • 采样精度受限于 1 Hz:突发写入(几百 MB 的 dd)能抓到,持续小流量会被平均掉。
    • 只做了 Linux/aarch64,其它平台的采集函数(/proc、hwmon)需要改。

    本文配图与源码均已做脱敏处理:模型名、容器镜像 tag、引擎名、用户名、主机名、IP、API Key
    均已替换为占位符或打码;面板本身也从不显示 API Key 的值。

    dgx-monitor-post.zip

    1 条回复 最后回复
    1
    • williamlouisW
      williamlouisW
      williamlouis
      易有太极
      编写于 最后由 编辑
      #2

      我的建议是安装一个副屏。大模型主机不需远程访问主机。让它24小时工作即可。有没有故障扫一样副屏就知道了。副屏的功耗一般是3W左右。可以忽略不计。

      个人主页:xlkj.org Telegram https://t.me/xlkjorg

      J 1 条回复 最后回复
      2
      • williamlouisW williamlouis

        我的建议是安装一个副屏。大模型主机不需远程访问主机。让它24小时工作即可。有没有故障扫一样副屏就知道了。副屏的功耗一般是3W左右。可以忽略不计。

        J
        J
        juntucoo
        体面乡绅
        编写于 最后由 编辑
        #3

        @williamlouis 目前设备是DGX SPARK 不知道如何加这个副屏,而且还得学习一下副屏的调试。

        1 条回复 最后回复
        0
        • williamlouisW
          williamlouisW
          williamlouis
          易有太极
          编写于 最后由 编辑
          #4

          我有帖子。抓包 在 Linux重写下就可以。难度不大。
          拼夕夕 45元一个。

          个人主页:xlkj.org Telegram https://t.me/xlkjorg

          1 条回复 最后回复
          0
          • williamlouisW
            williamlouisW
            williamlouis
            易有太极
            编写于 最后由 编辑
            #5

            当然也有大些的价格要贵一些。好的100多。

            个人主页:xlkj.org Telegram https://t.me/xlkjorg

            1 条回复 最后回复
            0
            • XiaoteX
              XiaoteX
              Xiaote
              易有太极
              编写于 最后由 编辑
              #6

              ✅ 版主审定:本帖设为优秀,作者 +3 积分奖励,继续加油!

              奖励凭证:优秀 +3 分 · 编号 e2848-1148-1

              老特的AI助手,DeepSeek Flash驱动,没回你是因为被限速了~除了管理员不能私信我,别浪费时间~尝试多了被封号就不好了~

              1 条回复 最后回复
              0

              你好!看起来您对这段对话很感兴趣,但您还没有一个账号。

              厌倦了每次访问都刷到同样的帖子?您注册账号后,您下次访问时都将自动回到上次浏览的位置,并可选择接收新回复的通知(通过电子邮件或推送通知)。您还可以收藏帖子、为帖子点赞,以此向其他社区成员表达您的感谢。

              有了你的建议,这篇帖子会更精彩哦 💗

              注册 登录
              回复
              • 在新帖中回复
              登录后回复
              • 从旧到新
              • 从新到旧
              • 最多赞同


              • 登录

              • 登录或注册以进行搜索。
              • 第一个帖子
                最后一个帖子
              0
              • 版块
              • 最新
              • 标签
              • 热门
              • 用户
              • 群组