# test-sts **Repository Path**: wanghuiic/test-sts ## Basic Information - **Project Name**: test-sts - **Description**: 测试k8s平台用statefulset启动多机多卡程序。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-05-30 - **Last Updated**: 2025-06-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # test-sts 测试k8s平台用StatefulSet启动多机多卡程序 ## 基本思路 torchrun运行多机多卡程序。例如,在两个节点上启动分布式训练任务,每个节点使用2个GPU。 * --nnodes:指定参与训练的节点总数。 * --node_rank:指定当前节点的排名,从0开始,主节点通常为0。 * --nproc_per_node:指定每个节点上启动的进程数,通常与该节点上的GPU数量相同。例如每个节点启动2个进程,每个进程对应一个GPU。 * --master_addr:指定主节点的地址,所有节点都需要连接到这个地址进行通信。这里使用了Kubernetes的内部域名,适用于在Kubernetes集群中运行。 * --master_port:指定主节点上的通信端口。 ``` torchrun --nproc_per_node=2 --nnodes=2 --node_rank=0 --master_addr=dist-example-0.dist-example.default.svc --master_port=1234 /workspace/test-dist.py torchrun --nproc_per_node=2 --nnodes=2 --node_rank=1 --master_addr=dist-example-0.dist-example.default.svc --master_port=1234 /workspace/test-dist.py ``` ### 为什么用StatefulSet StatefulSet 是 Kubernetes 中用于管理有状态应用的工作负载 API 对象。它提供了一种管理 Pod 集合的部署和扩缩的方法,并为这些 Pod 提供持久存储和持久标识符。与 Deployment 相似,StatefulSet 管理基于相同容器规范的一组 Pod。但与 Deployment 不同,StatefulSet 为每个 Pod 维护了一个有粘性的 ID,这些 Pod 是基于相同的规范创建的,但不能相互替换:无论怎样调度,每个 Pod 都有一个永久不变的 ID。 ### 用户工作目录 为了构造运行环境,需要为pod指定挂载卷以及挂载位置。 ``` spec: containers: volumeMounts: - name: workdir mountPath: /workspace volumes: - name: workdir persistentVolumeClaim: claimName: pvc-test-dist ``` 根据需要定义pvc,例如 ``` apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pvc-test-dist spec: accessModes: - ReadWriteMany storageClassName: nfs-storage resources: requests: storage: 100Mi ``` ### 模型加载 在指定位置提前准备模型文件。 例如,用户的源码里指定了模型的绝对路径为/workspace/Qwen2-7B/。 平台可以这样处理: 1. 模型放在nfs共享目录 /nfs/data/model/Qwen2-7B/,全局可用。 2. 定义Pod以volume方式将模型目录挂载到 /workspace/Qwen2-7B/。 ### 确定主节点 默认情况下,StatefulSet自动将pod命名为-0,-1,...,-N的形式。 dist-example-0.dist-example.default.svc 为了解析上面的域名,还需要定义一个headless服务。即规定 ``` clusterIP: None ``` 为StatefulSet创建Headless服务的目的是,允许Pod间通过稳定DNS记录直接通信。 ```yaml apiVersion: v1 kind: Service metadata: name: dist-example labels: app: dist-example spec: clusterIP: None selector: app: dist-example ports: - port: 1234 name: communication ``` ### 设置容器运行命令 让各个Pod统一挂载运行脚本,/scripts/entrypoint.sh ```bash POD_NAME=$(echo $POD_NAME) POD_INDEX=$(echo $POD_NAME | awk -F '-' '{print $NF}') echo "[entrypoint.sh]$POD_NAME $POD_INDEX" [ -n "$POD_INDEX" ] || { echo "error."; exit -1; } /bin/bash /scripts/init_$POD_INDEX.sh echo "[entrypoint.sh]finished" /bin/sleep infinity ``` 这里考虑的问题: 1. 要得到节点的index。可以解析sts的pod name。通过env环境变量传入Pod内部, ``` spec: containers: env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name ``` 2. 每个Pod运行不同的脚本文件。例如 scripts/init_0.sh ``` torchrun \ --nproc_per_node=2 \ --nnodes=2 \ --node_rank=0 \ --master_addr=dist-example-0.dist-example.default.svc \ --master_port=1234 \ /workspace/test-dist.py ``` scripts/init_1.sh ``` torchrun \ --nproc_per_node=2 \ --nnodes=2 \ --node_rank=1 \ --master_addr=dist-example-0.dist-example.default.svc \ --master_port=1234 \ /workspace/test-dist.py ``` 上面的命令行参数都可以由用户预先指定或自动生成。 entrypoint.sh,init_0.sh,init_1.sh等脚本文件可以放在ConfigMap, ``` kubectl create configmap dist-example-scripts --from-file=scripts/ ``` 然后挂载到Pod, ``` spec: containers: volumeMounts: - name: scripts mountPath: /scripts volumes: - name: scripts configMap: name: dist-example-scripts ``` ## 测试:test-sts ### 构建docker镜像 ``` docker build -t test-sts:v2 . ``` 如果k8s是较高版本例如1.28,可能需要将构建出来的镜像用ctr导入containerd, ``` docker save test-sts:v2 |ctr -n=k8s.io images import - ``` ### 部署nfs服务器 这里给出一个demo示例。假设服务器要共享的nfs目录是/nfs/data, ``` apt install -y nfs-kernel-server chown nobody:nogroup /nfs/data chmod 777 /nfs/data ``` 编辑/etc/exports文件,添加以下内容: ``` /nfs/data *(rw,sync,no_subtree_check) ``` 重启服务。 ``` exportfs -ra systemctl restart nfs-kernel-server ``` ``` # exportfs /nfs/data ``` ### 部署k8s nfs-storage 根据实际情况,修改 03.deployment.yaml中的ip地址部分。 然后执行下面的命令, ``` kubectl apply -f 01.rbac.yaml kubectl apply -f 02.storageclass.yaml kubectl apply -f 03.deployment.yaml ``` 测试一下, ``` kubectl apply -f 04.pvc-test.yaml ``` ### 准备模型 下载模型 ``` apt install git-lfs git lfs install git clone https://hf-mirror.com/Qwen/Qwen2-0.5B ``` 拷贝模型到nfs共享目录 ``` mkdir -p /nfs/data/model/ mv Qwen2-0.5B /nfs/data/model/ ``` ### 准备源码 创建pvc ```bash root@master01:~/test-sts# kubectl apply -f pvc.yaml persistentvolumeclaim/pvc-test-dist created ``` 找到实际位置, ```bash root@master01:~/test-sts# kubectl get pv $(kubectl get pvc pvc-test-dist -o json | jq -r '.spec.volumeName') -ojson | jq -r '.spec.nfs' { "path": "/nfs/data/default-pvc-test-dist-pvc-039a9aef-c4af-4dab-8076-2917318633d6", "server": "192.168.122.212" } ``` 然后将源码test-dist.py拷贝到上面的位置。 ```bash root@master01:~/test-sts# scp test-dist.py 192.168.122.212:/nfs/data/default-pvc-test-dist-pvc-039a9aef-c4af-4dab-8076-2917318633d6 ``` ### 准备启动脚本 根据实际情况,修改或增加启动脚本。例如,节点3的启动脚本是 init_3.sh。 ``` kubectl create configmap dist-example-scripts --from-file=scripts/ ``` ### 创建StatefulSet 根据实际情况,修改 sts.yaml,例如,GPU资源, ``` resources: requests: nvidia.com/gpu: 2 limits: nvidia.com/gpu: 2 ``` 如果平台没有GPU,修改为0,或直接删除。 如果平台有GPU,但希望使用纯CPU计算,则需要设置env, ``` spec: env: - name: NO_GPU value: "" ``` NO_GPU 只需要存在,取值可以为空串。 还需要修改模型的位置信息, ``` - name: qwen2-0-5b nfs: path: /nfs/data/model/Qwen2-0.5B server: 192.168.0.62 ``` 最后创建sts, ``` kubectl apply -f sts.yaml ``` 重新创建, ``` kubectl delete sts dist-example && kubectl apply -f sts.yaml ``` ### 输出示例 运行时的GPU利用率, ``` # nvidia-smi +-----------------------------------------------------------------------------------------+ | NVIDIA-SMI 550.78 Driver Version: 550.78 CUDA Version: 12.4 | |-----------------------------------------+------------------------+----------------------+ | GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |=========================================+========================+======================| | 0 Tesla T4 Off | 00000000:00:06.0 Off | 0 | | N/A 22C P0 37W / 70W | 2839MiB / 15360MiB | 28% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ | 1 Tesla T4 Off | 00000000:00:07.0 Off | 0 | | N/A 24C P0 41W / 70W | 2839MiB / 15360MiB | 33% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ | 2 Tesla T4 Off | 00000000:00:08.0 Off | 0 | | N/A 22C P0 36W / 70W | 2839MiB / 15360MiB | 27% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ | 3 Tesla T4 Off | 00000000:00:09.0 Off | 0 | | N/A 23C P0 40W / 70W | 2839MiB / 15360MiB | 99% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ +-----------------------------------------------------------------------------------------+ | Processes: | | GPU GI CI PID Type Process name GPU Memory | | ID ID Usage | |=========================================================================================| | 0 N/A N/A 62344 C /usr/bin/python 2836MiB | | 1 N/A N/A 62346 C /usr/bin/python 2836MiB | | 2 N/A N/A 62343 C /usr/bin/python 2836MiB | | 3 N/A N/A 62345 C /usr/bin/python 2836MiB | +-----------------------------------------------------------------------------------------+ ``` ## deepseek多机多卡测试——k8s平台 ### 要点 模型下载, ``` git clone https://hf-mirror.com/deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B ``` 将模型目录映射到Pod的 /root/.cache/huggingface 镜像 vllm/vllm-openai:latest 建立ray cluster,对于head节点, ``` ray start --block --head --port=6379 ``` 对于worker节点, ``` ray start --block --address=${HEAD_NODE_ADDRESS}:6379 ``` 最后在head节点启动 vllm serve 即推理服务。 ### 创建deepseek多机多卡在线服务 两个节点,每个节点有两张GPU。 注:测试环境的两个节点实际是两个Pod,且位于同一个物理节点,但足以说明问题。 ``` cd test-sts/deepseek kubectl delete sts deepseek-example kubectl delete configmap deepseek-example-scripts kubectl create configmap deepseek-example-scripts --from-file=scripts/ kubectl apply -f deepseek-sts.yaml ``` 查看日志,等待服务就绪, ``` test-sts/deepseek# kubectl logs -f deepseek-example-0 ... INFO: Started server process [865] INFO: Waiting for application startup. INFO: Application startup complete. ``` ### 运行示例 ```bash $ curl -sX POST "http://10.233.76.133:8080/v1/chat/completions" -H "Content-Type: application/json" -d '{ "model": "DeepSeek-R1", "messages": [{"role": "user", "content": "你是谁?"}] }' | jq . { "id": "chatcmpl-514c6bc906194cd1ab37ff9a46229010", "object": "chat.completion", "created": 1749538989, "model": "DeepSeek-R1", "choices": [ { "index": 0, "message": { "role": "assistant", "reasoning_content": null, "content": "我是DeepSeek-R1,一个由深度求索公司开发的智能助手,我擅长通过思考来帮您解答复杂的数学,代码和逻辑推理等理工类问题。\n\n\n我是DeepSeek-R1,一个由深度求索公司开发的智能助手,我擅长通过思考来帮您解答复杂的数学,代码和逻辑推理等理工类问题。", "tool_calls": [] }, "logprobs": null, "finish_reason": "stop", "stop_reason": null } ], "usage": { "prompt_tokens": 8, "total_tokens": 85, "completion_tokens": 77, "prompt_tokens_details": null }, "prompt_logprobs": null, "kv_transfer_params": null } ``` GPU显存占用情况, ```bash ~/test-sts$ nvidia-smi Tue Jun 10 16:48:16 2025 +-----------------------------------------------------------------------------------------+ | NVIDIA-SMI 550.78 Driver Version: 550.78 CUDA Version: 12.4 | |-----------------------------------------+------------------------+----------------------+ | GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |=========================================+========================+======================| | 0 Tesla T4 Off | 00000000:00:06.0 Off | 0 | | N/A 28C P0 26W / 70W | 12389MiB / 15360MiB | 0% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ | 1 Tesla T4 Off | 00000000:00:07.0 Off | 0 | | N/A 28C P0 25W / 70W | 12383MiB / 15360MiB | 0% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ | 2 Tesla T4 Off | 00000000:00:08.0 Off | 0 | | N/A 26C P0 24W / 70W | 12379MiB / 15360MiB | 0% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ | 3 Tesla T4 Off | 00000000:00:09.0 Off | 0 | | N/A 26C P0 25W / 70W | 12381MiB / 15360MiB | 0% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ +-----------------------------------------------------------------------------------------+ | Processes: | | GPU GI CI PID Type Process name GPU Memory | | ID ID Usage | |=========================================================================================| | 0 N/A N/A 6185 C /usr/bin/python3 12372MiB | | 1 N/A N/A 13103 C ray::RayWorkerWrapper 12366MiB | | 2 N/A N/A 13104 C ray::RayWorkerWrapper 12362MiB | | 3 N/A N/A 8260 C ray::RayWorkerWrapper 12364MiB | +-----------------------------------------------------------------------------------------+ ``` 参考[run_cluster.sh](https://gitee.com/mirrors/vllm/blob/main/examples/online_serving/run_cluster.sh) ## llama-factory微调测试 ### 镜像test-sts:sft 基于LLaMA-Factory的commit 7a7071e,做如下修改, ```diff diff --git a/data/dataset_info.json b/data/dataset_info.json index 7aaee14f..d136d7f2 100644 --- a/data/dataset_info.json +++ b/data/dataset_info.json @@ -678,5 +678,8 @@ "prompt": "content" }, "folder": "python" + }, + "stapes": { + "file_name": "stapes.json" } } diff --git a/examples/train_lora/llama3_lora_sft.yaml b/examples/train_lora/llama3_lora_sft.yaml index fe889208..d74dcd03 100644 --- a/examples/train_lora/llama3_lora_sft.yaml +++ b/examples/train_lora/llama3_lora_sft.yaml @@ -33,7 +33,8 @@ learning_rate: 1.0e-4 num_train_epochs: 3.0 lr_scheduler_type: cosine warmup_ratio: 0.1 -bf16: true +bf16: false +fp16: true ddp_timeout: 180000000 resume_from_checkpoint: null ``` 增加的文件 data/stapes.json merge.py test-sft.py 一起打包成 LLaMA-Factory.tar.gz文件并放在 test-sts/llamafactory/ 目录。 最后构建镜像, ``` docker build -t test-sts:sft . ``` ### 创建sts ``` cd llamafactory/ kubectl delete sts llamafactory-sft kubectl delete configmap llamafactory-sft-scripts kubectl create configmap llamafactory-sft-scripts --from-file=scripts/ kubectl apply -f train-sft-sts.yaml ``` 通过日志查看运行情况, ``` kubectl logs -f llamafactory-sft-0 ``` 主要过程是,微调、模型合并、运行推理。 运行结果示例。同一个问题,微调之前回答错误,微调之后回答正确。 ``` Prompt: 人体最小的骨头是什么? -------------------------------------------------- Original Model Output: 人体最小的骨头是什么?" 人体最小的骨头是股骨(或后腿骨)。它位于人体的后腿骨端,是最短的骨节。股骨是人体中长度最短的骨,但它在人体中扮演着重要的角色,帮助我们行走和站立。 -------------------------------------------------- Finetuned Model Output: 人体最小的骨头是什么?人体最小的骨头是镫骨,它位于中耳,是听小骨中最小的一块,对声音传导起着重要作用。 ```