本地化部署DeepSeek-R1 CPU架构
安装llama.cpp
我在 CPU架构LLaMA.cpp安装 按照以下方式完成编译安装:
Ubuntu Linux / Debian 编译环境:
apt-get update
apt-get install build-essential cmake curl libcurl4-openssl-dev -y
下载
llama.cpp
源代码:
llama.cpp
源代码git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
针对CPU架构编译:
# NOT USE -DGGML_CUDA=ON, ONLY support CPU
cmake llama.cpp -B llama.cpp/build \
-DBUILD_SHARED_LIBS=OFF -DLLAMA_CURL=ON
# -j 40 for system wich 48 cpu core
cmake --build llama.cpp/build --config Release --clean-first -j 40
#cp llama.cpp/build/bin/llama-* llama.cpp
下载模型 = =========
unsloth
在 huggingface.co 提供了8位量化 DeepSeek-R1-Q8_0
,可以通过以下方式下载:
DeepSeek-R1-Q8_0
# pip install huggingface_hub hf_transfer
# import os # Optional for faster downloading
# os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1"
from huggingface_hub import snapshot_download
snapshot_download(
repo_id = "unsloth/DeepSeek-R1-GGUF",
local_dir = "DeepSeek-R1-GGUF",
#allow_patterns = ["*UD-IQ1_S*"], # Select quant type UD-IQ1_S for 1.58bit
allow_patterns = ["*-R1-Q8_0"], # Select quant type R1-Q8_9 for 8bit
)
警告
实际选择应该根据自己的服务器内存规格来选择量化模型,必须确保模型文件大小不超过服务器内存(CPU架构)。
我挥泪出血满配了 HPE ProLiant DL360 Gen9服务器 768GB内存 尝试采用8位量化 DeepSeek-R1-Q8_0
备注
实际上墙内下载huggingface的模型文件非常吃力,我没有采用直接下载方法,而是通过阿里云租用的ECS虚拟机下载,然后再搬运回本地。代价是花费了2天时间以及约200RMB带宽和虚拟磁盘费用!
运行模型
./llama.cpp/build/bin/llama-server \
--model unsloth/DeepSeek-R1-GGUF/DeepSeek-R1-Q8_0/DeepSeek-R1.Q8_0-00001-of-00015.gguf \
--port 8081 \
--cache-type-k q8_0 \
--threads 16
参数:
--threads 16
表示llama-server
在回答问题时会并发动用多少个线程,也就是占用多少个CPU core,这里设置16
,则后续prompt
问题后,就会看到服务器上有16个cpu繁忙计算推理,负载会达到16
--port 8081
:llama-server
默认端口是8080
,不过8080
端口也是open-webui
使用的,所以采用错开配置如果使用GPU,可以再添加如下参数:
--ctx-size 1024
上下文长度,也就是token数量,根据硬件的RAM或VRAM使用量确定--n-gpu-layers
卸载到GPU上的层数量,加快推理速度,这个参数也取决于GPU内存,需要参考 Unsloth 的规格表格
备注
由于没有资金购买大容量GPU,所以采用满配 HPE ProLiant DL360 Gen9服务器 768GB内存 尝试采用8位量化 DeepSeek-R1-Q8_0
这里采用的参数非常简单,我还在学习摸索中,目前仅仅是运行起来...
模型文件非常巨大,SSD磁盘加载到内存需要很长时间
启动后
llama-server
内存使用大约680G
top - 18:25:39 up 7:19, 6 users, load average: 0.11, 0.16, 0.53
Tasks: 460 total, 1 running, 459 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 773887.5 total, 74480.7 free, 21397.6 used, 682890.4 buff/cache
MiB Swap: 0.0 total, 0.0 free, 0.0 used. 752489.9 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
5412 chief 20 0 679.9g 678.1g 664.3g S 0.0 89.7 10:50.35 llama-server
704 root 20 0 73232 37564 34420 S 0.0 0.0 0:01.05 systemd-journal
1 root 20 0 167940 12424 9212 S 0.0 0.0 0:07.55 systemd
1406 root 20 0 17820 10928 9332 S 0.0 0.0 0:00.03 sshd
交互
简单交互可以使用 curl :
curl -X POST "http://localhost:8081/completion" \
-H "Content-Type: application/json" \
-d '{"prompt": "编写bash脚本,检查字符串A是否包含在字符串B中,并给出脚本解释"}'
上述prompt要求DeepSeek编写一个简单的bash脚本,实际测试下来 当然非常缓慢 可以看到 llama.cpp
终端的统计:
llama.cpp
终端显示的日志显示了推理的消耗slot launch_slot_: id 0 | task 102 | processing task
slot update_slots: id 0 | task 102 | new prompt, n_ctx_slot = 4096, n_keep = 0, n_prompt_tokens = 19
slot update_slots: id 0 | task 102 | kv cache rm [1, end)
slot update_slots: id 0 | task 102 | prompt processing progress, n_past = 19, n_tokens = 18, progress = 0.947368
slot update_slots: id 0 | task 102 | prompt done, n_past = 19, n_tokens = 18
slot release: id 0 | task 102 | stop processing: n_past = 2194, truncated = 0
slot print_timing: id 0 | task 102 |
prompt eval time = 6987.96 ms / 18 tokens ( 388.22 ms per token, 2.58 tokens per second)
eval time = 3315563.11 ms / 2176 tokens ( 1523.70 ms per token, 0.66 tokens per second)
total time = 3322551.08 ms / 2194 tokens
srv update_slots: all slots are idle
srv log_server_r: request: POST /completion 127.0.0.1 200
可以看到最终这个简单的脚本问答花费了 55.38 分钟
,消耗了 2194 tokens
,平均只有 0.66 token/s
得到的输出结果如下(已经整理格式获取实际结果):
好的,我现在要编写一个bash脚本来检查字符串A是否包含在字符串B中。嗯,首先,我得回忆一下bash中字符串操作的方法。用户的需求是判断字符串A是否是字符串B的子串,然后可能需要根据结果做一些操作。
那我应该怎么判断呢?记得在bash中可以用通配符或者正则表达式来进行模式匹配。比如在条件判断语句中使用双中括号,里面可以用*通配符来检查是否包含。
举个例子,假设字符串B是\"hello world\",字符串A是\"world\"。那么应该返回存在包含关系。那么具体的语法应该是这样的吗:[[ \"$B\" == *\"$A\"* ]]。这应该可以判断B是否包含A的内容。
不过,得确保变量的正确引用,避免因为空格或者其他特殊字符导致的问题。所以在变量周围加上双引号应该比较好。然后,把整个条件判断放在双中括号中,这样支持通配符的扩展。
那脚本的结构大概是这样:首先获取用户输入的字符串A和B,然后进行判断,输出结果。或者,如果用户想通过参数传递这两个字符串的话,可能需要通过位置参数来获取。
假设用户希望通过命令行参数传递这两个字符串,那么脚本应该接受两个参数,第一个是A,第二个是B。或者反过来?需要明确参数的顺序。比如,执行脚本时输入./script.sh \"A\" \"B\",那么$1是A,$2是B。此时,检查$2是否包含$1?
或者题目中的要求是检查A是否包含在B中,那么参数顺序应该是A然后B。因此,在脚本中,将第一个参数作为子串,第二个作为原字符串。或者需要确认用户的需求,不过题目里确实说是检查字符串A是否包含在字符串B中。所以,正确的参数顺序应该是字符串A是子串,字符串B是主字符串。
所以,在脚本中,需要判断$2是否包含$1。或者反过来?可能我弄反了。比如,用户输入./check_substring.sh \"world\" \"hello world\",这个时候判断第二个参数是否包含第一个参数。
或者用户可能反过来,希望第一个参数是主字符串,第二个是子串?这可能吗?需要明确。不过题目中的描述是“字符串A是否包含在字符串B中”,即A是子串,B是主串。所以参数应该是A和B,顺序是A在前,B在后?
例如:脚本的参数是A和B,判断B是否包含A。所以,在判断条件里,应该检查$2是否包含$1?不,应该是检查字符串B是否包含A,所以B是主串,所以如果用户传递的第二个参数是B,那么正确的判断应该是[[ \"$2\" == *\"$1\"* ]]。
或者可能用户在调用脚本时,参数的顺序是主字符串后子串?这时候就需要确认参数顺序。不过按照题目的描述,正确的逻辑应该是:
字符串A是子串,字符串B是主串。判断A是否在B中。例如A是“test”,B是“this is a test string”,那么应该返回存在。
那么脚本的调用方式可能是:
./contains.sh \"$A\" \"$B\"
然后在脚本中,检查\"$B\"是否包含\"$A\"。即,在判断条件中,主字符串是第二个参数,子串是第一个参数?
或者相反?这时候需要明确参数顺序。如果用户的脚本使用方式是脚本后面跟两个参数,第一个是子串A,第二个是主串B,那么判断B是否包含A。那么正确的条件表达式是 [[ \"$B\" == *\"$A\"* ]]。即,第二个参数是否包含第一个参数。
所以,在脚本中,我需要获取两个参数,例如:
A=$1
B=$2
然后检查 [[ \"$B\" == *\"$A\"* ]]
如果是这样的话,当用户输入的顺序正确时,就能正确判断。例如,如果调用脚本是./check.sh \"lo\" \"hello\",那么B是\"hello\",A是\"lo\",而判断式变成 [[ \"hello\" == *\"lo\"* ]],结果是真,因此会输出存在包含关系。
那现在需要考虑如何处理参数的问题,以及可能的错误情况,比如参数数量不够。
所以,在脚本的开头,需要检查是否传入了两个参数。如果没有,给出错误提示,并退出。
比如:
if [ $# -ne 2 ]; then
echo \"用法: $0 <字符串A> <字符串B>\"
exit 1
fi
然后,将参数保存到变量中:
A=\"$1\"
B=\"$2\"
然后,判断B是否包含A:
if [[ \"$B\" == *\"$A\"* ]]; then
echo \"字符串B包含字符串A\"
else
echo \"字符串B不包含字符串A\"
fi
这样应该就能完成任务了。
那么整个脚本的结构大概是这样:
#!/bin/bash
if [ $# -ne 2 ]; then
echo \"错误:需要两个参数。\"
echo \"用法:$0 <字符串A> <字符串B>\"
exit 1
fi
A=\"$1\"
B=\"$2\"
if [[ \"$B\" == *\"$A\"* ]]; then
echo \"字符串 '$A' 包含在字符串 '$B' 中。\"
else
echo \"字符串 '$A' 不包含在字符串 '$B' 中。\"
fi
那这样就能处理了吗?例如,当A是空字符串的时候会如何?比如,如果用户传入空字符串作为A,那么任何B都会包含它?但是在实际情况中,可能需要处理这种情况。不过题目可能不要求处理这种情况,但脚本应该考虑。
例如,如果用户执行./check.sh \"\" \"abc\",那么条件判断就会成立,因为空字符串是任何字符串的子串。不过是否需要这样的处理呢?这取决于用户的需求。但按照常规逻辑,空字符串确实被任何字符串包含。
如果用户希望A不能为空,则需要在脚本中检查。但题目中并没有要求,所以可能不需要处理。
此外,当A或B中包含空格或特殊字符时,是否会被正确处理?因为我们在变量引用时用了双引号,所以应该可以处理带有空格的字符串。例如,参数用引号括起来的话,应该没问题。
例如,调用方式为:./check.sh \"a b\" \"test a b c\",这时,A的值是\"a b\",B是\"test a b c\",判断正确。
现在,编写这个脚本,然后进行测试。
总结脚本的步骤:
1. 检查参数数量是否正确,否则提示正确用法并退出。
2. 将参数赋给变量A和B。
3. 判断B是否包含A,使用模式匹配。
4. 输出结果。
这样应该就可以完成任务了。现在给出这个脚本,并加上注释解释。
例如:
#!/bin/bash
# 检查参数数量是否为2
if [ $# -ne 2 ]; then
echo \"错误:需要两个参数。\" >&2
echo \"用法:$0 <字符串A> <字符串B>\" >&2
exit 1
fi
# 获取输入的字符串A和B
A=\"$1\"
B=\"$2\"
# 判断B是否包含A
if [[ \"$B\" == *\"$A\"* ]]; then
echo \"字符串B '$B' 包含字符串A '$A'。\"
else
echo \"字符串B '$B' 不包含字符串A '$A'。\"
fi
这样应该就可以了。在注释中解释每一部分的作用。
比如,第一行是shebang,指定用bash执行。
检查参数数量是否等于2,否则显示错误信息。>&2是将错误信息输出到标准错误流,符合惯例。
然后变量赋值,A和B分别保存第一个和第二个参数。
条件判断使用双中括号,里面检查B是否匹配模式*\"$A\"*,也就是任意字符前后包围着A的内容。如果是,则成立。
最后根据结果输出对应的信息。
这样就完成了脚本的编写。
</think>
以下是检查字符串A是否包含在字符串B中的Bash脚本及其解释:
```bash
#!/bin/bash
# 检查是否提供了两个参数
if [ $# -ne 2 ]; then
echo \"错误:需要提供两个字符串参数。\" >&2
echo \"用法:$0 <字符串A> <字符串B>\" >&2
exit 1
fi
A=\"$1\"
B=\"$2\"
# 判断B是否包含A
if [[ \"$B\" == *\"$A\"* ]]; then
echo \"字符串B '$B' 包含字符串A '$A'。\"
else
echo \"字符串B '$B' 不包含字符串A '$A'。\"
fi
```
### 脚本解释
1. **Shebang指令**
`#!/bin/bash` 指定脚本使用Bash解释器执行。
2. **参数检查**
`if [ $# -ne 2 ]; then` 检查参数数量是否为2。
`$#` 是传递给脚本的参数个数,`-ne 2` 表示不等于2。
如果参数数量错误,输出错误信息到标准错误(`>&2`)并退出。
3. **变量赋值**
`A=\"$1\"` 和 `B=\"$2\"` 将第一个和第二个参数分别赋值给变量A和B。
使用双引号包裹变量,确保处理包含空格的字符串时不会出错。
4. **包含性检查**
`[[ \"$B\" == *\"$A\"* ]]` 使用Bash的双中括号和通配符`*`进行模式匹配。
如果字符串B包含字符串A,条件成立,否则不成立。
5. **结果输出**
根据条件判断结果,输出相应的提示信息。
### 使用示例
```bash
$ ./check_substring.sh \"world\" \"hello world\"
字符串B 'hello world' 包含字符串A 'world'。
$ ./check_substring.sh \"apple\" \"banana\"
字符串B 'banana' 不包含字符串A 'apple'。
```
### 注意事项
- 若字符串A为空,脚本将认为任何字符串B都包含A(因空字符串是任意字符串的子串)。
- 参数中包含特殊字符(如空格、星号等)时,需用引号包裹以确保正确传递。
下一步
由于自己的资金有限,不可能购买昂贵的GPU设备,所以我准备继续用这套CPU架构的二手服务器来做探索:
通过软件配置和部署来优化性能,力争能够达到每秒5个tokens(对上述问题可能在5分钟内完成)
必要时准备升级到更好一点的 E5v4 处理器