VLM 服务器

VLM 服务器用 mlx-vlm 加载 Gemma 4 E4B(mlx 4bit),让它看着盘面图像和文本选出一步。它通过与围棋 GO19 同一框架的 OT88 协议接收来自 Qt 应用和网页的请求。提示词中会一并放入合法着法列表。

加载失败

服务器日志中有两次启动服务器的记录,两次都因同样的错误而失败。

ValueError: Missing 54 parameters

缺失的权重是语言模型第 24~41 层的 self_attn k_proj、v_proj、k_norm。目前安装的 mlx-vlm 是 0.5.0。看起来是模型文件与 mlx-vlm 预期的结构不一致。

即使失败,服务器也会启动

服务器如果加载不了模型,就会转入“随机合法着法模式”,照样打开端口。监控脚本只看端口是否响应,所以判断它为正常。在画面上选择 VLM 时,返回的是随机的一个合法着法。

日志里只留下了超过 3 万次的连接和断开,无法分辨哪次连接是真正的对局请求。这就是无法知道第 1 篇的 31 局中有几局是与 VLM 对局的原因。

换成随机的途径

即使模型加载成功,也有三条途径会换成随机着法。

  • 推理过程中出错时
  • 明明有地方可下,模型却选择停一手时
  • 模型选了不能下的位置或禁止的坐标时

如果 VLM 的回答是不能下的位置,网页画面会再换回本地 AI。目前部署的本地 AI 是第 2 篇中只看一步的 AI。无论哪种情况,画面上都显示为 VLM 下的。在围棋中我们决定绝不偷偷换成任意着法,而黑白棋却做成了相反的样子。

这一阶段的实际代码

这是模型加载失败时清空 model 并让服务器继续运行的部分。

vlm_server/server.pypython · 8 行
model: Optional[_MlxVlmModel] = None
if load_model:
    model = _MlxVlmModel(model_id, max_new_tokens=max_new_tokens, temperature=temperature)
    try:
        model.start(wait_load=True)
    except BaseException as e:
        LOGGER.exception("모델 로드 실패 → 무작위 합법수 모드: %s", e)
        model = None

这是接收请求并决定着法的部分。如果没有模型,或者回答不是合法着法,就随机选择。

vlm_server/server.pypython · 36 行
model = self.server.model
row = col = None
is_pass = False
if model is not None:
    try:
        row, col, is_pass = model.predict_move(
            img, board, ai_side, legal, forbidden=list(bans), timeout_s=180.0
        )
    except Exception as e:
        LOGGER.warning("VLM 추론 실패, 합법수 무작위: %s", e)
        row = col = None
        is_pass = False

if is_pass:
    LOGGER.info("모델 PASS (합법수 있음) → 합법수 무작위")
    row = col = None

if row is None or col is None or (row, col) not in legal or (row, col) in bans:
    if row is not None and col is not None:
        LOGGER.info("불법/금지 (%s,%s) → 합법수 무작위", row, col)
    pick = _choose_legal(legal, bans)
    if pick is None:
        _send_pass(sock)
        continue
    row, col = pick

# ...

def _choose_legal(
    legal: list[tuple[int, int]], forbidden: set[tuple[int, int]]
) -> Optional[tuple[int, int]]:
    remain = [m for m in legal if m not in forbidden]
    pool = remain or legal
    if not pool:
        return None
    return random.choice(pool)

下一篇是“4. 把从围棋、五子棋中学到的东西移植到黑白棋的计划”。