Docker 零帧起手 Docker 的用途 我们经常诟病深度学习代码的不可复现性,某次在小红书见人锐评——学术圈的哥们要是真在乎自己工作的 real impact,都应该学会如何用 docker,而不是留下一堆没法复现的代码让几年后的 reviewer 拿出来恶心人。从我上述讨论中,想必大家已经感受到了,docker 是一种将开发环境连同代码复制并分发的强大软件。在我的日常工作中,docker 起到了如下作用: 提供高度可复现且隔离的开发环境:Docker 不仅确保了代码的可复现性,还提供了一个隔离的沙箱环境。在开发过程中,我们可以在容器内自由安装依赖、修改系统配置,而不会影响宿主机的环境。
我们经常诟病深度学习代码的不可复现性,某次在小红书见人锐评——学术圈的哥们要是真在乎自己工作的 real impact,都应该学会如何用 docker,而不是留下一堆没法复现的代码让几年后的 reviewer 拿出来恶心人。从我上述讨论中,想必大家已经感受到了,docker 是一种将开发环境连同代码复制并分发的强大软件。在我的日常工作中,docker 起到了如下作用:
总之,为了共同塑造良好的科研环境,避免有人用 baseline "在我的机器上能跑"来恶心别人,学习 docker 对任何人都是必不可少的。
nerdctl xxx 即可。绝大多数 docker 镜像都被发布在 Docker Hub。为了开发 sglang,我通常使用 SGLang 的官方镜像 lmsysorg/sglang Tags | Docker Hub。当然,如果你关注 SGLang RL 小组的工作,其实我们也有专门为了 verl-SGLang 搭建的镜像,比如 jurong 的 ocss884/verl-sglang:ngc-th2.6.0-cu126-sglang0.4.5.post3。你当然发现了,这个版本带有 SGLang 的版本号,并不是最新的 SGLang。因此,这个 docker 我更多用于 verl—SGLang 的 CI。实际开发 verl—SGLang,我还是会用 SGLang 本身的 docker,然后在其中安装 verl 和最新的 SGLang。
# 下载镜像 # docker pull <image-name> docker pull lmsysorg/sglang:latest
下载的镜像相当于压缩包,我们要把镜像解成容器才可以运行。
运行容器的指令格式是:
docker run [OPTIONS] IMAGE [COMMAND]
-it 交互式终端:有该参数你才能在 docker 里用交互式终端# 使用 -it:可以进入容器并执行命令 docker run -it ubuntu bash # 此时你可以输入命令,比如 ls、cd 等 # 不使用 -it:容器会立即退出 docker run ubuntu bash # 容器会立即结束,因为无法接收输入
--name <container-name> 容器名,方便下次重启。标记 docker 的用途或者拥有者。
命名规则请参考服务器准则。在 SGLang 的开发机器上,随意命名的容器会被直接删除。
--shm-size <shared-memory-size> 共享 CPU 内存大小。一般我们做 RL 需要较大的内存,默认的 64MB 会导致崩溃,建议设置为 16g 及以上。
--gpus all 允许容器 access 哪些 GPU。如无特殊需求,设置成 all 即可。
-v <host-path>:<container-path> 目录挂载:这可能是最重要的功能。举个例子,前文就提到了,我们将开发集群上的统一登录用户的 huggingface cache 挂载到了每个 docker 下,避免了每个人一个 cache 满天飞。更具体的来说,-v 可将宿主机目录 <host-path> 的全部内容挂载到容器目录 <container-path> 。宿主机和 docker 容器会共享目录下所有的文件和文件夹。容器对内容修改对宿主机可见,反之亦然。该参数可以多次添加,通常用于映射代码工作区,数据集,模型文件,配置文件等。
我们将这些常见参数组合起来,得到如下指令:
docker run -it --name <container-name> --shm-size 16g --gpus all -v <host-path>:<container-path> IMAGE
-p <host-port>:<container-port> 端口映射:将容器端口 <container-port> 映射到宿主机端口 <host-port> 。使得外部可以通过宿主机端口来访问容器内运行的服务。
--network host 网络共享:使容器直接使用宿主机的网络。共享 ip,端口,网络资源等。
--network host 以共享网络代理。--network host 时,-p 参数会被忽略。-e <cv-name>=<cv-value> 环境变量:设置容器内环境变量<cv-name> 的值为 <cv-value> ,可多次添加。
--ipc=host 进程间通信的命名空间共享:允许容器内的进程与宿主机上的进程进行通信,共享 IPC 命名空间。
-d 在后台运行容器,输入 exit 时容器不关闭。
--rm 容器关闭后自动删除。
在启动 container 的同时,我们可以指定 container 立即执行的指令。譬如:
bash 终端,可以输入指令,管理文件等。docker run -it [other OPTIONS] <image-name> bash
docker run -p 30000:30000 --env "HF_TOKEN=hf_xxx" [other OPTIONS] <image-name> python3 -m sglang.launch_server [other paras]
在 docker 中,容器的生命周期管理是最基础的操作。这里是几个核心命令的区别:
docker run:创建并启动新容器:
Error: Conflict. The container name is already in use,因为不能创建同名容器docker start:启动已停止的容器:
Error: No such container;docker restart:重启容器
Error: No such container;docker exec:在运行中的容器执行命令
Error: No such container;Error: Container is not running;查看容器:
docker ps 查看正在运行的容器docker ps -a 查看所有容器(包括已停止的)docker ps -a -s 查看所有容器及其大小关闭容器:
exit 或按 Ctrl + D-d 参数启动容器删除容器:
docker stop <container-name>docker rm <container-name>--rm 参数:容器关闭后自动删除# 1. 首次创建并运行容器 docker run -it --name my_container ubuntu bash # 2. 容器停止后,重新启动 docker start my_container # 3. 容器运行中,需要重启 docker restart my_container # 4. 容器运行中,需要打开新的终端 docker exec -it my_container bash # 5. 如果容器已存在,想创建新容器 docker run -it --name my_container_new ubuntu bash
--rm 参数时,容器停止后会自动删除,此时 start 和 restart 都会失败exec 只能用于运行中的容器,不能用于已停止的容器run 是创建新容器,其他命令都是操作已存在的容器在我们的日常开发中,为 CI 测试机器发布稳定的 docker 镜像必不可少。具体而言,细心的读者可能早已发现,verl-sglang 的不少 docker 都是我们自己 build 的,比如 ocss884/verl-sglang:ngc-th2.6.0-cu126-sglang0.4.5.post3。自己 build docker 可以要安装特定版本的 CUDA、PyTorch 驱动,或者加入一些自己写的脚本、代码,配置环境变量等。比如说,我们希望:
/workspaceDocker 构建镜像的过程就基于 Dockerfile 忠实地执行这些操作。
Dockerfile 的常见指令:
FROM <base-image>:<tag>:指定基础镜像,告诉 Docker 要基于哪个现有的镜像开始构建;WORKDIR /path/to/workdir:设置工作目录,后续指令都会在这个目录下执行。如果目录不存在,WORKDIR 会自动创建;RUN <command>:在镜像内部执行命令,每条 RUN 指令都会在当前镜像的基础上创建一个新的层。为了减少镜像层数和体积,加快构建速度,通常把多个 apt-get install 或者 pip install 命令用 && 连在一条 RUN 指令里;COPY <host-path> <container-path>:将宿主机的文件或目录复制到镜像内的指定路径;CMD ["command", "param1", "param2"]:指定容器启动时默认执行的命令。还可写成 CMD command param1 param2 (shell form)。注意,一个 Dockerfile 里只能有一条 CMD 指令,如果有多条,只有最后一条生效。如果在 docker run 时指定了命令,那么 CMD 的命令会被覆盖;ENV <key>=<value>:设置容器运行时的环境变量,镜像构建时不生效;ARG <key>=<value>:设置镜像构建时的临时变量,容器运行时不生效;# 导入 Nvidia 官方镜像 # 该镜像把 CUDA 12.1.1 和 cuDNN 8.9.0 都装好了,不需要再处理版本兼容问题 FROM nvidia/cuda:12.1.1-cudnn8-devel-ubuntu22.04 # 设置工作目录为 /sgl-workspace WORKDIR /sgl-workspace # 设置环境变量,如 huggingface token 用于下载模型 ENV HF_TOKEN=hf_xxxyyyzzz # 安装开发工具 RUN apt-get update && apt-get install -y \ vim \ tmux \ wget \ git \ && rm -rf /var/lib/apt/lists/* \ && apt-get clean # 将当前目录下的 app.py 文件复制到容器内的工作目录 COPY app.py /sgl-workspace/app.py # 默认执行命令,启动 Python 脚本 CMD ["python3", "app.py"]
在命令行构建 docker 的指令:
docker build -t <image-name>:<tag> -f <docker-file> <path>
-t <image-name>:<tag>:镜像的名字和标签;-f <docker-file>:Dockerfile 的路径;<path>:指定 Docker 构建镜像时的构建上下文路径,即 Docker 可以访问的文件和目录的根路径。它决定了 Dockerfile 中文件引用(如 COPY、ADD)的查找范围。通常设为 . 表示当前目录,但也可以是其他本地目录或 Git 仓库 URL;镜像构建好以后,我们可以把本地镜像 docker push到远程镜像仓库,如 Docker Hub 等。
对于 Docker Hub,镜像名通常是 <dockerhub-username>/<repo-name>:<tag>。 如果你在 docker build 时已经用了这个格式,那这步可以跳过。如果没用,或者你想推送到不同的仓库/用户下,就需要重新打标签。
docker tag <source-image>:<tag> <target-image>:<tag>
在 docker hub 注册账号后,使用 docker login 在本地登录
docker login
确保 <image-name>:<tag> 是你上一步打好标签的、符合仓库规范的完整名称。然后 push 即可
docker push <image-name>:<tag>