本地化部署DeepSeek-R1 CPU架构

安装llama.cpp

我在 CPU架构LLaMA.cpp安装 按照以下方式完成编译安装:

编译环境准备
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架构编译:

针对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 ,可以通过以下方式下载:

下载 8位量化 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检查验证
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

得到的输出结果如下(已经整理格式获取实际结果):

要求DeepSeek编写脚本输出结果内容
好的,我现在要编写一个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 处理器

参考