当前位置:   article > 正文

基于魔搭开源推理引擎 DashInfer实现CPU服务器大模型推理--实战篇

dashinfer

前言

        本篇关于Python语言接口使用 DashInfer引擎实现CPU大模型推理的是实战教学,如果想要了解相关技术原理的可以参考上篇文章:基于魔搭开源推理引擎 DashInfer实现CPU服务器大模型推理--理论篇-CSDN博客

 一、环境配置

  • 模型: Qwen1.5-1.8-Chat
  • 推理引擎: DashInfer
  • 硬件: x86, Emerald Rapids, 96 vCPU @ 3.2GHz, 16GBx24 DDR
  • CPU内存32G,16核
  • 服务器基础环境ubuntu22.04,torch-cpu-2.1.2,python3.10

1.1、python 依赖

  1. dashinfer
  2. sentencepiece
  3. accelerate
  4. transformers_stream_generator
  5. tiktoken

这里的主要的引擎dashinfer 我在win下pip install 一直失败,不知道是不是我环境的问题,还是本身包不支持win

还有就是protobuf好像需要3.18.0,因为我在测试出现过这个包协议错误问题,

TensorFlow官方的建议是不使用,可能会存在冲突,所以可能需要卸载

1.2、硬件支持

  • x86 CPU:要求硬件至少需要支持AVX2指令集。对于第五代至强(Xeon)处理器(Emerald Rapids)、第四代至强(Xeon)处理器(Sapphire Rapids)等(对应于阿里云第8代ECS实例,如g8i),采用AMX矩阵指令加速计算。
  • ARMv9 CPU:要求硬件支持SVE指令集。支持如倚天(Yitian)710等ARMv9架构处理器(对应于阿里云第8代ECS实例,如g8y),采用SVE向量指令加速计算。

1.3、数据类型

  • x86 CPU:支持FP32、BF16。
  • ARM Yitian710 CPU:FP32、BF16、InstantQuant(量化技术)。

二、推理实战

2.1、在单NUMA CPU上进行单NUMA推理

示例中默认使用的是单NUMA推理。 在单NUMA CPU上进行单NUMA推理不需要特殊的权限和配置。

2.2、在多NUMA CPU上进行单NUMA推理

在多NUMA节点的CPU上,若只需要1个NUMA节点进行推理,需要用numactl进行绑核。 该方式需要在docker run的时候添加--cap-add SYS_NICE --cap-add SYS_PTRACE --ipc=host参数。

使用单NUMA推理,需要将模型配置文件中的device_ids字段改成要运行的NUMA节点编号,并将multinode_mode字段设置为true。

"device_ids": [
    0
],
"multinode_mode": true,

2.3、多NUMA推理

使用多NUMA推理,需要用mpirun+numactl进行绑核,以达到最佳性能。 该方式需要在docker run的时候添加--cap-add SYS_NICE --cap-add SYS_PTRACE --ipc=host参数。

使用多NUMA推理,需要将模型配置文件中的device_ids字段改成要运行的NUMA节点编号,并将multinode_mode字段设置为true。

"device_ids": [ 0, 1 ], "multinode_mode": true,

2.4、原始模型下载

  1. ## download model from modelscope
  2. original_model = {
  3. "source": "modelscope",
  4. "model_id": "qwen/Qwen1.5-1.8B-Chat",
  5. "revision": "master",
  6. "model_path": ""
  7. }
  8. def download_model(model_id, revision, source="modelscope"):
  9. """
  10. x下载原始模型
  11. :param model_id:
  12. :param revision:
  13. :param source:
  14. :return:
  15. """
  16. print(f"Downloading model {model_id} (revision: {revision}) from {source}")
  17. if source == "modelscope":
  18. from modelscope import snapshot_download
  19. model_dir = snapshot_download(model_id, revision=revision)
  20. elif source == "huggingface":
  21. from huggingface_hub import snapshot_download
  22. model_dir = snapshot_download(repo_id=model_id)
  23. else:
  24. raise ValueError("Unknown source")
  25. print(f"Save model to path {model_dir}")
  26. return model_dir

2.5、prompt的组织格式

  1. start_text = "<|im_start|>"
  2. end_text = "<|im_end|>"
  3. system_msg = {"role": "system", "content": "You are a helpful assistant."}
  4. user_msg = {"role": "user", "content": ""}
  5. assistant_msg = {"role": "assistant", "content": ""}
  6. prompt_template = Template(
  7. "{{start_text}}" + "{{system_role}}\n" + "{{system_content}}" + "{{end_text}}\n" +
  8. "{{start_text}}" + "{{user_role}}\n" + "{{user_content}}" + "{{end_text}}\n" +
  9. "{{start_text}}" + "{{assistant_role}}\n")

2.6、模型转换配置文件

config_qwen_v15_1_8b.json

  1. {
  2. "model_name": "Qwen1.5-1.8B-Chat",
  3. "model_type": "Qwen_v15",
  4. "model_path": "./dashinfer_models/",
  5. "data_type": "float32",
  6. "device_type": "CPU",
  7. "device_ids": [
  8. 0
  9. ],
  10. "multinode_mode": false,
  11. "engine_config": {
  12. "engine_max_length": 2048,
  13. "engine_max_batch": 8,
  14. "do_profiling": false,
  15. "num_threads": 0,
  16. "matmul_precision": "medium"
  17. },
  18. "generation_config": {
  19. "temperature": 0.95,
  20. "early_stopping": true,
  21. "top_k": 1024,
  22. "top_p": 0.8,
  23. "repetition_penalty": 1.1,
  24. "presence_penalty": 0.0,
  25. "min_length": 0,
  26. "max_length": 2048,
  27. "no_repeat_ngram_size": 0,
  28. "eos_token_id": 151643,
  29. "seed": 1234,
  30. "stop_words_ids": [
  31. [
  32. 151643
  33. ],
  34. [
  35. 151644
  36. ],
  37. [
  38. 151645
  39. ]
  40. ]
  41. },
  42. "convert_config": {
  43. "do_dynamic_quantize_convert": false
  44. },
  45. "quantization_config": {
  46. "activation_type": "bfloat16",
  47. "weight_type": "uint8",
  48. "SubChannel": true,
  49. "GroupSize": 256
  50. }
  51. }

2.7、完整实战代码

传参配置格式化代码

文件名我这命名为llm_utils.py

  1. # coding: utf-8
  2. # @Time :
  3. # @Author :
  4. # @description :
  5. from typing import Tuple
  6. class Role:
  7. USER = 'user'
  8. SYSTEM = 'system'
  9. BOT = 'bot'
  10. ASSISTANT = 'assistant'
  11. ATTACHMENT = 'attachment'
  12. default_system = 'You are a helpful assistant.'
  13. def history_to_messages(history, system: str):
  14. """
  15. 格式化传参中的history和系统提示词
  16. :param history: [[问题,回答],[问题,回答]]
  17. :param system:
  18. :return:
  19. """
  20. messages = [{'role': Role.SYSTEM, 'content': system}]
  21. for h in history:
  22. messages.append({'role': Role.USER, 'content': h[0]})
  23. messages.append({'role': Role.ASSISTANT, 'content': h[1]})
  24. return messages
  25. def messages_to_history(messages):
  26. """
  27. 历史聊天保留
  28. :param messages:
  29. :return:
  30. """
  31. assert messages[0]['role'] == Role.SYSTEM
  32. system = messages[0]['content']
  33. history = []
  34. for q, r in zip(messages[1::2], messages[2::2]):
  35. history.append([q['content'], r['content']])
  36. return system, history
  37. def message_to_prompt(messages) -> str:
  38. """
  39. 提示词模板格式化
  40. :param messages:
  41. :return:
  42. """
  43. prompt = ""
  44. for item in messages:
  45. im_start, im_end = "<|im_start|>", "<|im_end|>"
  46. prompt += f"\n{im_start}{item['role']}\n{item['content']}{im_end}"
  47. prompt += f"\n{im_start}assistant\n"
  48. return prompt

推理实战run.py

  1. # coding: utf-8
  2. # @Time :
  3. # @Author :
  4. # @description :
  5. import os
  6. import copy
  7. import threading
  8. import subprocess
  9. import random
  10. from typing import Optional
  11. from llm_utils import history_to_messages, Role, message_to_prompt, messages_to_history
  12. os.system("pip uninstall -y tensorflow tensorflow-estimator tensorflow-io-gcs-filesystem")
  13. os.environ["LANG"] = "C"
  14. os.environ["LC_ALL"] = "C"
  15. from dashinfer.helper import EngineHelper, ConfigManager
  16. log_lock = threading.Lock()
  17. config_file = "config_qwen_v15_1_8b.json"
  18. config = ConfigManager.get_config_from_json(config_file)
  19. def download_model(model_id, revision, source="modelscope"):
  20. """
  21. x下载原始模型
  22. :param model_id:
  23. :param revision:
  24. :param source:
  25. :return:
  26. """
  27. print(f"Downloading model {model_id} (revision: {revision}) from {source}")
  28. if source == "modelscope":
  29. from modelscope import snapshot_download
  30. model_dir = snapshot_download(model_id, revision=revision)
  31. elif source == "huggingface":
  32. from huggingface_hub import snapshot_download
  33. model_dir = snapshot_download(repo_id=model_id)
  34. else:
  35. raise ValueError("Unknown source")
  36. print(f"Save model to path {model_dir}")
  37. return model_dir
  38. cmd = f"pip show dashinfer | grep 'Location' | cut -d ' ' -f 2"
  39. package_location = subprocess.run(cmd,
  40. stdout=subprocess.PIPE,
  41. stderr=subprocess.PIPE,
  42. shell=True,
  43. text=True)
  44. package_location = package_location.stdout.strip()
  45. os.environ["AS_DAEMON_PATH"] = package_location + "/dashinfer/allspark/bin"
  46. os.environ["AS_NUMA_NUM"] = str(len(config["device_ids"]))
  47. os.environ["AS_NUMA_OFFSET"] = str(config["device_ids"][0])
  48. ## download model from modelscope
  49. original_model = {
  50. "source": "modelscope",
  51. "model_id": "qwen/Qwen1.5-1.8B-Chat",
  52. "revision": "master",
  53. "model_path": ""
  54. }
  55. original_model["model_path"] = download_model(original_model["model_id"],
  56. original_model["revision"],
  57. original_model["source"])
  58. # GPU模型转换成CPU格式文件
  59. engine_helper = EngineHelper(config)
  60. engine_helper.verbose = True
  61. engine_helper.init_tokenizer(original_model["model_path"])
  62. ## convert huggingface model to dashinfer model
  63. ## only one conversion is required
  64. engine_helper.convert_model(original_model["model_path"]) # 转换,这里我记得文件不会覆盖,如果多次运行可能出现多个文件,运行后可以注释掉
  65. engine_helper.init_engine()
  66. engine_max_batch = engine_helper.engine_config["engine_max_batch"]
  67. def model_chat(query, history=None,system='You are a helpful assistant.') :
  68. """
  69. 生成器输出流结果
  70. :param query:
  71. :param history:
  72. :param system:
  73. :return:
  74. """
  75. if query is None:
  76. query = ''
  77. if history is None:
  78. history = []
  79. messages = history_to_messages(history, system)
  80. messages.append({'role': Role.USER, 'content': query})
  81. prompt = message_to_prompt(messages)
  82. gen_cfg = copy.deepcopy(engine_helper.default_gen_cfg)
  83. gen_cfg["seed"] = random.randint(0, 10000)
  84. request_list = engine_helper.create_request([prompt], [gen_cfg])
  85. request = request_list[0]
  86. # request_list[0].out_text
  87. gen = engine_helper.process_one_request_stream(request)
  88. return gen
  89. def model_chat_all(query, history=None,system='You are a helpful assistant.') :
  90. """
  91. 直接return结果,也是需要结果输出完成后才能打印
  92. :param query:
  93. :param history:
  94. :param system:
  95. :return:
  96. """
  97. if query is None:
  98. query = ''
  99. if history is None:
  100. history = []
  101. messages = history_to_messages(history, system)
  102. messages.append({'role': Role.USER, 'content': query})
  103. prompt = message_to_prompt(messages)
  104. gen_cfg = copy.deepcopy(engine_helper.default_gen_cfg)
  105. gen_cfg["seed"] = random.randint(0, 10000)
  106. request_list = engine_helper.create_request([prompt], [gen_cfg])
  107. request = request_list[0]
  108. # request_list[0].out_text
  109. gen = engine_helper.process_one_request_stream(request)
  110. # 需要从生成器中取数据才能有结果
  111. responses = [i for i in gen if i] # 不取数据不会返回结果
  112. result = request_list[0].out_text
  113. return result
  114. if __name__ == '__main__':
  115. from IPython.display import display, clear_output
  116. input_value = '写个四字成语'
  117. gen = model_chat(input_value)
  118. for part in gen: # 流式查看数据
  119. clear_output(wait=True)
  120. print(f"Input: {input_value}")
  121. print(f"Response:\n{part}")

 git地址:GitHub - liukangjia666/dashinfer_demo: 魔搭CPU大模型推理引擎dashinfer的测试实战代码

2.8、推理效果分析

(1)内存占用情况,我这个使用的1.8b的模型占用内存大概在8-15g左右

(2)推理速度依赖CPU的计算能力

(3)推理结果精度和GPU推理是有些区别,但整体来说影响不大

(4)json文件中的推理参数可以自定义修改,如temperature,top_p等

2.9、模型配置文件

<path_to_dashinfer>/examples/python/model_config目录下提供了一些config示例。

以下是对config中的参数说明:

  1. - `model_name`: DashInfer模型名称,自定义;
  2. - `model_type`: DashInfer模型类型,可选项:LLaMA_v2、ChatGLM_v2、ChatGLM_v3、Qwen_v10、Qwen_v15、Qwen_v20
  3. - `model_path`: DashInfer模型导出路径;
  4. - `data_type`: 输出的数据类型,可选项:float32
  5. - `device_type`: 推理硬件,可选项:CPU;
  6. - `device_ids`: 用于推理的NUMA节点,可以通过Linux命令`lscpu`查看CPU的NUMA信息;
  7. - `multinode_mode`: 是否在多NUMA CPU上进行推理,可选项:truefalse
  8. - `convert_config`: 模型转换相关参数;
  9. - `do_dynamic_quantize_convert`: 是否量化权重,可选项:truefalse,目前仅ARM CPU支持量化;
  10. - `engine_config`: 推理引擎参数;
  11. - `engine_max_length`: 最大推理长度,<= 11000
  12. - `engine_max_batch`: 最大batch数;
  13. - `do_profiling`: 是否对推理过程进行profiling,可选项:truefalse,若要进行profiling,需要设置`do_profiling = true`,并且设置环境变量`AS_PROFILE=ON`;
  14. - `num_threads`: 线程数,设置为单NUMA节点下的物理核数量时性能最佳,当设置的数值为0时,EngineHelper会自动解析`lscpu`的输出并设置,当设置的数值 > 0 时,采用设置的数值;
  15. - `matmul_precision`: 矩阵乘法的计算精度,可选项:high、medium,设置为high时,采用fp32进行矩阵乘法计算,设置为medium时,采用bf16进行计算;
  16. - `generation_config`: 生成参数;
  17. - `temperature`: 随机过程 temperature;
  18. - `early_stopping`: 在生成stop_words_ids后是否停止生成,可选项:truefalse
  19. - `top_k`: 采样过程,top k 参数,top_k = 0 时对全词表排序;
  20. - `top_p`: 采样过程,top p 参数,0 <= top_p <= 1.0top_p = 0 表示不使用topp;
  21. - `repetition_penalty`: The parameter for repetition penalty. 1.0 means no penalty.
  22. - `presence_penalty`: The parameter for presence penalty. 0.0 means no penalty.
  23. - `min_length`: 输入+输出的最小长度,默认为0,不启用filter;
  24. - `max_length`: 输入+输出的最大长度;
  25. - `no_repeat_ngram_size`: 用于控制重复词生成,默认为0If set to int > 0, all ngrams of that size can only occur once.
  26. - `eos_token_id`: EOS对应的token,取决于模型;
  27. - `seed`: 随机数seed;
  28. - `stop_words_ids`: List of token ids of stop ids.
  29. - `quantization_config`: 量化参数,当 do_dynamic_quantize_convert = true 时需要设置;
  30. - `activation_type`: 矩阵乘法的输入矩阵数据类型,可选项:bfloat16
  31. - `weight_type`: 矩阵乘法的权重数据类型,可选项:uint8
  32. - `SubChannel`: 是否进行权重sub-channel量化,可选项:truefalse
  33. - `GroupSize`: sub-channel量化粒度,可选项:64128256512

代码开源地址:

https://github.com/modelscope/dash-infer

推理体验地址:

https://www.modelscope.cn/studios/modelscope/DashInfer-Demo

如有问题可评论区留言,或者关注下面公众号获取加群二维码进群交流

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号