环境与部署目标

项目 本文环境
宿主系统 Ubuntu 22.04 LTS
显卡 RTX 4090,驱动报告显存 49,140 MiB,约 48GB
系统内存 约 32GB
NVIDIA 驱动 610.57.04
Docker Engine 29.8.0
容器基础环境 Ubuntu 24.04、CUDA 13.1.2
引擎 alanthinker/ninfer-4090-yarn,面向 sm_89
模型 Qwen3.8-27B,groupwise-int 格式的 .ninfer 权重
客户端 Cherry Studio 2.0.14
工作目录 /root/llm

本文使用的是 48GB 显存的 4090。 常见的 24GB 4090 可以参考引擎仓库的单路部署示例,但不能把本文的 8 路并发、状态缓存和显存实测数字直接套用过去

这里的 256K 指 262,144 token,是每个会话的上下文上限。输入、历史消息、工具定义以及生成内容都要在这个窗口内安排。虽然仓库名称带有 YaRN,本文没有启用位置缩放,保持原生 256K 配置。

先确认 Docker 能访问显卡

本文从宿主机已经安装 NVIDIA 驱动、Docker Engine 和 NVIDIA Container Toolkit 开始。尚未安装时,可按 Docker 的 Ubuntu 安装文档NVIDIA Container Toolkit 安装文档准备环境。

先在宿主机检查:

1
2
nvidia-smi
docker version

再检查容器是否能访问 GPU:

1
2
3
docker run --rm --gpus all \
docker.m.daocloud.io/nvidia/cuda:13.1.2-runtime-ubuntu24.04 \
nvidia-smi

能看到显卡信息后,再继续构建推理镜像。编译工具链放在 Docker 构建阶段,宿主机不需要另外安装同一版本的 CUDA Toolkit。

准备目录与源码

下面的命令按 root 用户编写。服务器地址以本文的 192.168.6.53 为例,部署到其他机器时,应改为那台机器实际拥有的内网地址。

1
2
3
4
5
6
mkdir -p /root/llm/models /root/llm/.cache/huggingface
cd /root/llm

git clone https://github.com/alanthinker/ninfer-4090-yarn.git
cd ninfer-4090-yarn
git checkout 53c871ae79dd2e34ff24ab787545013235bc6e78

这里固定了本文使用的源码版本。后面的国内源配置和下载脚本需要按本文补充,避免直接使用仓库后续更新后的默认行为。

GitHub 访问需要代理时,可以只给这次命令设置代理,例如:

1
2
3
https_proxy=http://YOUR_PROXY_HOST:10808 \
http_proxy=http://YOUR_PROXY_HOST:10808 \
git clone https://github.com/alanthinker/ninfer-4090-yarn.git

这是上一条 git clone 的替代写法,不需要重复克隆。YOUR_PROXY_HOST 要替换成自己的代理地址;代理协议应与代理软件提供的端口类型一致。

配置镜像构建与国内源

本机采用以下下载源:

用途 地址
CUDA 基础镜像 docker.m.daocloud.io/nvidia/cuda
容器 Ubuntu 软件包 mirrors.tuna.tsinghua.edu.cn
pip、uv 软件包 pypi.tuna.tsinghua.edu.cn
模型文件 hf-mirror.com

镜像站的可用性可能变化。这里把地址放在脚本和构建参数中,方便替换,不需要给宿主机设置永久代理。

1. 容器软件源脚本

/root/llm/ninfer-4090-yarn/scripts/configure-cn-sources.sh 写入:

1
2
3
4
5
6
7
8
9
10
#!/bin/sh
set -eu

for source in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do
[ -f "$source" ] || continue
sed -i \
-e 's|https\?://\([a-z][a-z]\.\)\?archive.ubuntu.com/ubuntu|https://mirrors.tuna.tsinghua.edu.cn/ubuntu|g' \
-e 's|https\?://security.ubuntu.com/ubuntu|https://mirrors.tuna.tsinghua.edu.cn/ubuntu|g' \
"$source"
done

2. Dockerfile

/root/llm/ninfer-4090-yarn/Dockerfile 替换为下面的内容。这是本文实际构建使用的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
ARG CUDA_REGISTRY=docker.m.daocloud.io

FROM ${CUDA_REGISTRY}/nvidia/cuda:13.1.2-devel-ubuntu24.04 AS build

ARG DEBIAN_FRONTEND=noninteractive
ENV HF_ENDPOINT=https://hf-mirror.com \
PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple \
UV_DEFAULT_INDEX=https://pypi.tuna.tsinghua.edu.cn/simple
COPY scripts/configure-cn-sources.sh /tmp/configure-cn-sources.sh
RUN sh /tmp/configure-cn-sources.sh && rm /tmp/configure-cn-sources.sh
RUN apt-get update \
&& apt-get install --yes --no-install-recommends \
cmake \
libavcodec-dev \
libavformat-dev \
libavutil-dev \
libcurl4-openssl-dev \
libswscale-dev \
ninja-build \
pkg-config \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /src
COPY . .

RUN cmake -S . -B /build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DNINFER_BUILD_APPS=ON \
-DBUILD_TESTING=OFF \
-DNINFER_BUILD_BENCHMARKS=OFF \
&& cmake --build /build --parallel --target ninfer ninfer-serve

FROM ${CUDA_REGISTRY}/nvidia/cuda:13.1.2-runtime-ubuntu24.04

ARG DEBIAN_FRONTEND=noninteractive
ENV HF_ENDPOINT=https://hf-mirror.com \
HF_HOME=/workspace/.cache/huggingface \
PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple \
UV_DEFAULT_INDEX=https://pypi.tuna.tsinghua.edu.cn/simple
COPY scripts/configure-cn-sources.sh /tmp/configure-cn-sources.sh
RUN sh /tmp/configure-cn-sources.sh && rm /tmp/configure-cn-sources.sh
RUN apt-get update \
&& apt-get install --yes --no-install-recommends \
ca-certificates \
libavcodec60 \
libavformat60 \
libavutil58 \
libcurl4t64 \
libswscale7 \
&& rm -rf /var/lib/apt/lists/*

# The CUDA runtime image ships forward-compatibility libraries in
# /usr/local/cuda*/compat (a newer libcuda.so than the host driver). Forward
# compatibility is supported only on datacenter GPUs; on any GeForce card the
# loader picks these up and every CUDA call fails at startup with
# cudaErrorCompatNotSupportedOnDevice: forward compatibility was attempted
# on non supported HW
# Removing them lets the container use the host driver through ordinary CUDA
# minor-version compatibility, which is what an RTX 3090/3090 Ti needs.
RUN rm -rf /usr/local/cuda-13.1/compat /usr/local/cuda-13/compat /usr/local/cuda/compat

COPY --from=build /build/apps/ninfer /usr/local/bin/ninfer
COPY --from=build /build/apps/ninfer-serve /usr/local/bin/ninfer-serve

WORKDIR /workspace
EXPOSE 8080
STOPSIGNAL SIGTERM

CMD ["ninfer-serve", "--help"]

这个 Dockerfile 分为编译和运行两个阶段。最终镜像只保留可执行程序及运行依赖;移除 CUDA forward-compatibility 库的步骤也予以保留,让容器使用宿主机驱动。

3. 构建脚本

/root/llm/build-ninfer.sh 写入:

1
2
3
4
5
6
7
#!/usr/bin/env bash
set -euo pipefail
project_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
exec docker build \
--build-arg "CUDA_REGISTRY=${CUDA_REGISTRY:-docker.m.daocloud.io}" \
--tag "${NINFER_IMAGE:-ninfer-4090:sm89}" \
"$project_dir/ninfer-4090-yarn"

然后构建:

1
2
3
cd /root/llm
chmod +x build-ninfer.sh
./build-ninfer.sh

ninfer-4090:sm89 是本地构建生成的镜像标签。首次构建需要下载基础镜像、安装依赖并编译 CUDA 代码,耗时会受到网络、CPU 和磁盘速度影响。

如果默认镜像站不可用,可以改用其他可用的站点:

1
CUDA_REGISTRY=docker.1ms.run ./build-ninfer.sh

下载与引擎匹配的模型

本文使用的模型仓库是 neroued/Qwen3.8-27B-NInfer,文件名为 qwen3_8_27b.ninfer,约 16.96 GiB。

这里需要固定模型版本:该引擎不能读取模型仓库新版 main 中增加的 DFlash2 对象。 文件名相同,并不代表内部格式仍与旧引擎兼容。

/root/llm/ninfer-4090-yarn/scripts/download-qwen38.sh 写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/usr/bin/env bash
set -euo pipefail

# Model downloads must connect directly, even if the caller exported a proxy.
unset http_proxy https_proxy all_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY

root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
model_dir="${NINFER_MODEL_DIR:-$root/models}"
model="$model_dir/qwen3_8_27b.ninfer"
endpoint="${HF_ENDPOINT:-https://hf-mirror.com}"
# This engine predates DFlash2. Newer main artifacts contain unsupported objects.
expected_sha256=eec39564993d6e9c7d5e383382a760f093465c9d163ec9a1bd6b80199514bf3e
revision=18dfc887423fa5aabf3cb56fac41490e462b3fab

mkdir -p -- "$model_dir"
if [[ -f "$model" ]] && printf '%s %s\n' "$expected_sha256" "$model" | sha256sum --check --status; then
printf 'Model already verified: %s\n' "$model"
exit 0
fi
printf '%s\n' 'Downloading Qwen3.8-27B NInfer model...'
url="${endpoint%/}/neroued/Qwen3.8-27B-NInfer/resolve/$revision/qwen3_8_27b.ninfer"
download() {
if command -v aria2c >/dev/null 2>&1; then
aria2c --no-conf=true --continue=true --auto-file-renaming=false --file-allocation=none \
--max-connection-per-server=8 --split=8 --min-split-size=64M \
--max-tries=5 --retry-wait=5 --connect-timeout=20 \
--summary-interval=30 --console-log-level=warn \
--dir="$model_dir" --out=qwen3_8_27b.ninfer.part "$url"
elif [[ -f "$model.part.aria2" ]]; then
printf '%s\n' 'Install aria2 to resume this multipart download.' >&2
return 1
else
curl --disable --noproxy '*' -L -C - --fail --retry 5 --connect-timeout 20 --output "$model.part" "$url"
fi
}
if ! download; then
printf '%s\n' 'Download failed. Run this script again to resume.' >&2
exit 1
fi
printf '%s %s\n' "$expected_sha256" "$model.part" | sha256sum --check
mv -- "$model.part" "$model"
printf 'Model ready: %s\n' "$model"

执行下载:

1
2
3
cd /root/llm
NINFER_MODEL_DIR=/root/llm/models \
bash ninfer-4090-yarn/scripts/download-qwen38.sh

脚本固定模型修订版本,并在下载结束后校验 SHA-256。校验通过才会把 .part 文件改为正式模型文件名。下载中断后重新执行即可续传;如果此前使用 aria2 下载,需要继续使用 aria2 完成。

模型下载显式绕过代理,默认直接连接 HF 镜像,避免大文件占用代理流量。已有模型也会先做校验,通过后跳过下载。

启动推理服务

/root/llm/mirrors.env 写入:

1
2
3
4
HF_ENDPOINT=https://hf-mirror.com
HF_HOME=/workspace/.cache/huggingface
PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
UV_DEFAULT_INDEX=https://pypi.tuna.tsinghua.edu.cn/simple

/root/llm/run-ninfer.sh 写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/usr/bin/env bash
set -euo pipefail

project_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
image="${NINFER_IMAGE:-ninfer-4090:sm89}"
model_dir="${MODEL_DIR:-$project_dir/models}"
cache_dir="${HF_CACHE_DIR:-$project_dir/.cache/huggingface}"
run_options=()
if [[ $# -eq 1 && "$1" == --detach ]]; then
run_options+=(--detach)
elif [[ $# -ne 0 ]]; then
echo "用法:$0 [--detach]" >&2
exit 2
fi

if docker container inspect ninfer-4090 >/dev/null 2>&1; then
if [[ "$(docker inspect --format '{{.State.Running}}' ninfer-4090)" == true ]]; then
echo "ninfer-4090 已在运行;查看日志:docker logs -f ninfer-4090"
exit 0
fi
echo "启动已有容器 ninfer-4090,沿用创建时的参数。"
if [[ ${#run_options[@]} -gt 0 ]]; then
exec docker start ninfer-4090
fi
exec docker start --attach ninfer-4090
fi

if [[ ! -f "$model_dir/qwen3_8_27b.ninfer" ]]; then
echo "缺少模型:$model_dir/qwen3_8_27b.ninfer,请先放入模型文件。" >&2
exit 1
fi

if ! docker image inspect "$image" >/dev/null 2>&1; then
NINFER_IMAGE="$image" "$project_dir/build-ninfer.sh"
fi

mkdir -p "$cache_dir"
exec docker run "${run_options[@]}" --name ninfer-4090 \
--restart always \
--log-driver json-file --log-opt max-size=10m --log-opt max-file=3 \
--gpus all --publish 192.168.6.53:8080:8080 \
--env-file "$project_dir/mirrors.env" \
--workdir /workspace \
--volume "$model_dir:/workspace/models:ro" \
--volume "$cache_dir:/workspace/.cache/huggingface" \
"$image" \
ninfer-serve models/qwen3_8_27b.ninfer \
--host 0.0.0.0 --port 8080 \
--max-context 262144 --kv-capacity auto \
--default-max-tokens 65536 \
--max-concurrency 8 --max-pending-requests 16 \
--device-state-slots 16 --host-state-slots 32 \
--pending-timeout-ms 600000 \
--prefill-chunk 1024 --kv-dtype rk4v4-e8 \
--spec mtp --draft-tokens 3 --lm-head-draft \
--vision --preserve-thinking

后台启动:

1
2
3
cd /root/llm
chmod +x run-ninfer.sh
./run-ninfer.sh --detach

然后观察日志:

1
docker logs -f ninfer-4090

看到 engine readylistening on http://0.0.0.0:8080 后,服务才完成初始化。本机一次启动约花费 44 秒,仅供判断启动过程参考。

容器内部的 0.0.0.0:8080 用于接收 Docker 转发;实际发布到宿主机的地址是 192.168.6.53:8080。本文没有启用 API Key 鉴权,服务面向可信局域网使用。

关键参数如何理解

参数 本文取值 作用
--max-context 262144 每个会话的上下文上限
--default-max-tokens 65536 请求未指定输出上限时,思考与回答合计最多生成 64K token
--kv-capacity auto 根据可用显存建立共享 KV 池,并预留约 1 GiB 安全余量
--max-concurrency 8 最多同时执行的请求数
--max-pending-requests 16 等待队列容量
--pending-timeout-ms 600000 队列等待超时为 10 分钟
--device-state-slots 16 活跃请求之外的显卡状态、检查点容量
--host-state-slots 32 主机内存中的状态槽容量
--prefill-chunk 1024 输入预填充的分块大小
--kv-dtype rk4v4-e8 E8 四位 KV 存储模式
--spec mtp --draft-tokens 3 MTP3 每轮最多提出 3 个草稿 token,验证后接受
--lm-head-draft 开启 使用优化后的草稿输出头
--vision 开启 加载视觉能力
--preserve-thinking 开启 在后续轮次保留已结束回复的思考内容

CUDA Graph 默认开启。--preserve-thinking 控制历史思考内容的保留,不等于强制每次请求开启思考。

256K 与 8 路并发的关系

本机启动日志给出的共享 KV 池为 1,418,496 token。活跃请求和保留的前缀缓存共同使用这部分空间。

因此,单个会话的上限是 262,144,但 8 个会话不能同时各占满 262,144:后者合计需要 2,097,152 token,超过当前池容量。把池容量直接除以 8,只能得到约 177,312 token 的算术均值,并不代表引擎把每路固定限制在这个长度。

只有一个会话活跃时,8 路并发上限不会把它的窗口降为八分之一。如果业务要求多个会话同时跑满 256K,应另外按共享缓存容量规划并发,并实测长上下文负载。

验证接口

先检查健康状态:

1
curl --noproxy '*' http://192.168.6.53:8080/health

正常返回:

1
{"status":"ok"}

再获取模型信息:

1
curl --noproxy '*' http://192.168.6.53:8080/v1/models

关键字段应为:

1
2
3
4
5
6
{
"id": "qwen3.8-27b",
"context_window": 262144,
"max_model_len": 262144,
"modalities": {"vision": true}
}

上面只展示 data 数组中模型对象的部分字段。最后用 Chat Completions 发送一个短请求:

1
2
3
4
5
6
7
8
curl --noproxy '*' http://192.168.6.53:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "qwen3.8-27b",
"messages": [{"role": "user", "content": "请只回复:连接成功"}],
"max_tokens": 64,
"chat_template_kwargs": {"enable_thinking": false}
}'

--noproxy '*' 让这几次局域网检查直接访问服务器。如果能够返回正常的 choices,再接入客户端。

接入 Cherry Studio 2.0.14

在“设置 → 模型服务”中添加 OpenAI 兼容服务,填写:

配置项 内容
API 地址 http://192.168.6.53:8080/v1
API Key 服务未开启鉴权;客户端必填时可填 ninfer
模型 ID qwen3.8-27b
聊天接口 OpenAI Chat Completions

获取并添加模型后,检查该模型使用的聊天接口,选择 Chat Completions。这里的模型 ID 要与 /v1/models 返回的一致。

遇到 additional response fields 报错

如果出现:

1
the requested additional response fields have no available response representation

对应的服务端错误是:

1
2
3
4
5
6
7
{
"error": {
"code": "include_not_supported",
"param": "include",
"type": "invalid_request_error"
}
}

这是 Responses 请求携带非空 include 时触发的参数校验。当前引擎的 Responses 接口只接受省略该字段或空数组。

在 Cherry Studio 中将聊天接口改为 Chat Completions 即可避开这处不兼容。本机已复现该错误,并确认 Chat Completions 请求正常返回 HTTP 200。

为什么获取模型后显示 100 万上下文

Cherry Studio 2.0.14 的模型服务页面可能把 Qwen3.8-27B 标为 1,000,000 token,即使本地 /v1/models 已返回 262,144。

核对该版本源码后,可以看到两步处理:

  1. OpenAI 兼容模型获取逻辑只把模型 ID、名称和归属方映射到客户端模型对象,没有读取本地服务的上下文长度字段。
  2. 客户端随后用内置模型库补充配置;该版本的 Qwen3.8-27B 条目写的是 contextWindow: 1000000

所以,这个数字来自客户端的通用模型资料,不能用来判断本地部署实际开放的长度。给 NInfer 多返回几个同义字段,也不会自动解决 2.0.14 的这条读取链路。

在已添加模型的编辑页面,手动覆盖“上下文窗口”为 262144。对于另外两个字段,可以按日常使用需要制定客户端预算,例如:

字段 一组日常使用的客户端预算
上下文窗口 262144
最大输入 token 196608
最大输出 token 65536

输入预算按 262144 - 65536 计算,为思考与回复预留 64K。这组输入、输出数值是客户端使用策略,不是模型分别拥有的两个独立窗口,也不是服务端声明的硬性输出上限。 本文用 --default-max-tokens 65536 将未指定请求输出长度时的预算设为 64K;需要长回复时,可以调整客户端预算,但输入与输出仍共用 256K,模板和工具内容也要计入。

模型页的“最大输出”是客户端能力配置,单轮实际输出还会受助手设置和请求参数影响。模型页填写 65536,并不等于每次都会生成 65536 token。请同时检查助手或聊天参数中的输出上限,避免实际请求仍发送较低的数值。

如果要实现“获取模型时自动填好这三个字段”,需要在 Cherry Studio 客户端同时补上远端字段映射、远端部署限制的优先级,以及输入/输出限制的保存逻辑。本文保留现有客户端,采用手动覆盖,不修改推理服务。

绘图或代码生成到一半停止

SVG、HTML 等绘图代码本质上仍是文本输出。实际使用中,一次绘图请求启用了 xhigh 思考,日志显示 max output 8,192,最后恰好生成 8192 token,以 output limit 结束。思考过程也占用输出预算,因此正文或代码可能尚未完成。

本文将服务端默认输出预算提高到 65536。这不扩大 256K 总上下文,也不要求降低并发:那次请求触发的是输出长度限制,而非显存不足。

服务端默认值只对没有指定输出上限的请求生效。若 Cherry Studio 的助手或聊天参数明确发送了 max_tokens: 8192,或者 max_completion_tokens: 8192,仍会在 8K 停止。模型编辑页的最大输出与实际聊天请求的输出参数都应检查;绘图、长代码任务可以使用 65536,并按需要降低思考强度。

调整后,可以在日志的请求开始行确认 max output 65,536。这个数字表示本次请求实际采用的预算。64K 能缓解旧的 8K 截断问题,但长任务仍可能用完预算;达到总上下文上限或客户端中断连接也仍会结束请求。

实测速度与预期

本文单路短提示词测试使用固定的三组任务,每次输出上限为 768 token,关闭思考,温度 0.7、top-p 0.8、top-k 20、presence penalty 1.5、seed 42。三个任务都生成到 768 token,因此这里只比较生成速度,不把它们当作完整任务的质量评测。

任务 MTP3 解码速度 草稿接受率
中文科普长文 94.3 token/s 39.8%
Python 代码 142.2 token/s 77.0%
JSONL 数据 142.0 token/s 77.1%

每项只测了一次。这组结果说明速度会随内容变化:草稿越容易被主模型接受,每轮能够提交的 token 往往越多。日常中文对话只有 100 多 token/s,并不能单凭这个数字判断部署有问题。

也测试过把草稿长度从 3 提高到 5:中文降到 80.6 token/s,代码为 146.0 token/s,JSONL 为 139.8 token/s。增加草稿长度没有带来普遍收益,因此最终恢复 MTP3。

另一次 8 路并发测试中,每路短提示词 57 token、输出 512 token、关闭思考,8 个请求均成功:

统计口径 结果
总输出 4096 token
客户端总耗时,含输入处理和等待 14.701 秒
按总耗时计算的聚合速度 278.62 token/s
服务端一个完整 5 秒窗口的聚合解码速度 321.6 token/s
每路请求的解码速度 约 38–41 token/s
显存峰值 46,911 / 49,140 MiB

聚合速度描述整张卡同时服务多个请求的吞吐,不能当作一个聊天窗口的输出速度。以上也没有验证 8 路同时满 256K 或图片推理负载。

当前运行的是 groupwise-int + MTP3。网上的 NVFP4Full + DFlash 成绩使用了不同的量化与推测解码路径,比较时还需要对齐显卡、模型、上下文长度和任务内容。

日常维护与开机自启

服务容器采用 --restart always,停止后会保留。容器异常退出时 Docker 会尝试重新启动;手动执行 docker stop 后,在当前 Docker 服务运行期间保持停止,方便把显卡让给其他任务。下次开机或 Docker 服务重启后,模型会再次自动启动,即使此前是手动停止的。

这套规则适合平时常驻、偶尔停下来跑其他 GPU 任务的场景。如果希望手动停止后连下一次开机也不启动,则应使用 unless-stopped;本文选择 always

确认 Docker 已设置为开机启动:

1
systemctl is-enabled docker

如果尚未启用,执行:

1
systemctl enable --now docker

Docker 的重启策略处理进程退出,不会仅因 HTTP 接口无响应就自动重启。每次恢复服务仍需从本地磁盘加载模型,等待日志出现 engine ready 后再使用。

查看、停止与启动

查看状态和日志:

1
2
docker ps -a --filter name=ninfer-4090
docker logs --tail 100 ninfer-4090

临时停止服务,保留容器并释放模型占用的显存:

1
docker stop ninfer-4090

启动已有容器:

1
docker start ninfer-4090

也可以继续使用脚本:

1
2
cd /root/llm
./run-ninfer.sh --detach

脚本会检查同名容器:不存在时创建,已停止时启动,正在运行时提示后退出。因此可以重复执行,不会创建多个模型实例。

需要直接重启进程时:

1
docker restart ninfer-4090

上述启动命令默认不等待模型加载完成。可以用 docker logs -f ninfer-4090 观察启动过程,再用前面的 /health 检查确认服务就绪。

修改启动参数或更新镜像

已有容器会沿用创建时的参数。编辑 run-ninfer.shmirrors.env,或者重新构建同名镜像后,单纯执行 docker start 不会把这些变更应用到旧容器。

需要应用新配置时,先结束正在进行的请求,再重建容器:

1
2
3
4
cd /root/llm
docker stop ninfer-4090
docker rm ninfer-4090
./run-ninfer.sh --detach

这几条命令只替换服务容器,不删除宿主机挂载的模型和 HF 缓存。单纯调整启动参数不需要重新构建镜像;修改引擎代码或 Dockerfile 时,先执行 ./build-ninfer.sh 构建,再重建容器。

可以检查最终状态:

1
docker inspect ninfer-4090 --format 'restart={{.HostConfig.RestartPolicy.Name}} auto_remove={{.HostConfig.AutoRemove}}'

应看到 restart=always auto_remove=false。文中前面的 GPU 环境检查仍使用 docker run --rm,因为那个容器只执行一次 nvidia-smi,不承担常驻服务。

参考资料