第五章模型推理与性能优化学习心得

整体感受

这段代码主要实现了一个可以在网页上交互的对话机器人,用到了昇思 MindSpore 框架和 Gradio 工具。简单来说,就是让我们能通过网页和模型聊天!下面我就一段一段给大家解释。

代码逐段解读

1. 导入需要的工具库

import gradio as grimport mindsporefrom mindnlp.transformers import AutoModelForCausalLM, AutoTokenizerfrom mindnlp.transformers import TextIteratorStreamerfrom threading import Threadfrom mindnlp.peft import PeftModel

这部分就像我们做菜前要准备好各种厨具,这里导入了运行程序需要的工具库:

  • gradio as gr:用来快速搭建网页交互界面的工具,有了它不用懂复杂的网页开发也能做出漂亮的交互界面

  • mindspore:昇思深度学习框架,是运行模型的基础

  • mindnlp.transformers里的工具:用来加载大模型(AutoModelForCausalLM)和处理文本(AutoTokenizer)

  • TextIteratorStreamer:让模型生成回答时能像人说话一样一个字一个字出来,不是等全部生成完才显示

  • Thread:多线程工具,让模型生成回答时网页不会卡住

  • PeftModel:用来加载微调后的模型(这里暂时没用到)

2. 环境设置(注释掉的部分)

# activate synchronization in pynative mode# mindspore.set_context(pynative_synchronize=True)

这部分是设置 MindSpore 运行模式的,现在注释掉了,我们知道有这么个设置就行,暂时不用管它~

3. 加载模型和分词器

tokenizer = AutoTokenizer.from_pretrained("MindSpore-Lab/DeepSeek-R1-Distill-Qwen-1.5B-FP16", mirror="modelers", ms_dtype=mindspore.float16)model = AutoModelForCausalLM.from_pretrained("MindSpore-Lab/DeepSeek-R1-Distill-Qwen-1.5B-FP16", mirror="modelers", ms_dtype=mindspore.float16)# model = PeftModel.from_pretrained(model, "./output/DeepSeek-R1-Distill-Qwen-1.5B/adapter_model_for_demo/") # adapter_model path

这部分是整个程序的核心,就像我们把 “大脑” 和 “翻译官” 请出来:

  • tokenizer:相当于翻译官,负责把我们说的话转换成模型能理解的数字格式

  • model:就是大模型本身,相当于机器人的 “大脑”,负责思考并生成回答

  • 括号里的参数是告诉程序从哪里下载模型,用什么数据类型运行(float16 是一种节省内存的格式)

  • 最后一行注释是加载微调模型用的,我们初学者先掌握基础的就行~

4. 设置系统提示词

system_prompt = "You are a helpful and friendly chatbot"

这个很简单,就是给机器人设定一个初始身份,告诉它 “你是一个乐于助人且友好的聊天机器人”,相当于给机器人定个性格~

5. 构建对话历史格式

def build_input_from_chat_history(chat_history, msg: str):    messages = [{'role': 'system', 'content': system_prompt}]    for user_msg, ai_msg in chat_history:        messages.append({'role': 'user', 'content': user_msg})        messages.append({'role': 'assistant', 'content': ai_msg})    messages.append({'role': 'user', 'content': msg})    return messages

这个函数的作用是整理聊天记录,把之前的对话和当前的问题按照模型要求的格式组装起来:

  • 先加入系统提示词

  • 然后把历史对话一条一条加进去,区分用户说的话(user)和机器人说的话(assistant)

  • 最后加上当前用户的问题

  • 这样模型就能理解整个对话上下文啦

6. 生成回答的函数

def predict(message, history):    history_transformer_format = history + [[message, ""]]    messages = build_input_from_chat_history(history, message)    input_ids = tokenizer.apply_chat_template(            messages,            add_generation_prompt=True,            return_tensors="ms",            tokenize=True        )    streamer = TextIteratorStreamer(tokenizer, timeout=300, skip_prompt=True, skip_special_tokens=True)    generate_kwargs = dict(        input_ids=input_ids,        streamer=streamer,        max_new_tokens=1024,        do_sample=True,        top_p=0.9,        temperature=0.1,        num_beams=1,        repetition_penalty=1.2    )    t = Thread(target=model.generate, kwargs=generate_kwargs)    t.start()    partial_message = ""    for new_token in streamer:        partial_message += new_token        if '</s>' in partial_message:            break        yield partial_message

这是处理用户输入并生成回答的核心函数,有点复杂,我们慢慢看:

  • 首先把历史对话和当前问题整理好

  • 调用前面的函数生成模型需要的消息格式

  • 用tokenizer.apply_chat_template把消息转换成模型能处理的数字

  • streamer是用来实现流式输出的,让回答一个字一个字显示出来

  • generate_kwargs里是生成回答的各种参数:

    • max_new_tokens=1024:最多生成 1024 个词

    • temperature=0.1:温度值越小,回答越确定;越大越随机

  • 用多线程(Thread)来生成回答,这样网页不会卡住

  • 最后把生成的内容一点点返回给界面显示

7. 搭建并启动界面

gr.ChatInterface(predict,                 title="DeepSeek-R1-Distill-Qwen-1.5B",                 description="问几个问题",                 examples=['你是谁?', '你能做什么?']                 ).launch()

这部分就是搭建网页界面了:

  • gr.ChatInterface:创建一个聊天界面

  • predict:指定用上面的 predict 函数来处理输入和生成回答

  • title:网页标题

  • description:描述文字

  • examples:提供一些示例问题

  • .launch():启动这个网页应用,运行后我们就能在浏览器里和机器人聊天啦

整体感受

这段代码同样是实现 DeepSeek-R1-Distill-Qwen-1.5B 模型的推理,但明显针对香橙派的硬件特点做了很多优化。比如增加了静态图加速、单 token 生成计时等功能,看起来更注重在资源有限的开发板上高效运行。

代码逐段解读

1. 导入工具库

import gradio as grimport mindsporefrom mindnlp.transformers import AutoTokenizer, AutoModelForCausalLM, StaticCachefrom mindnlp.core import opsfrom mindnlp.configs import set_pyboost, ON_ORANGE_PI import timeimport numpy as np

和上一段代码相比,多了几个新朋友:

  • StaticCache:静态缓存工具,能帮模型在生成文本时节省内存

  • ops:MindNLP 的操作工具库,提供各种计算功能

  • set_pyboost和ON_ORANGE_PI:专门用于香橙派环境配置的工具

  • time:用来计算生成每个 token 的时间

  • numpy:这里用来处理概率计算,在香橙派上比原生实现更快

2. 环境与参数设置

# 在香橙派上,开启 O2 级别的 jit 优化if ON_ORANGE_PI:    mindspore.set_context(        enable_graph_kernel=True,        mode=mindspore.GRAPH_MODE,        jit_config={            "jit_level": "O2",        },    )# 生成参数配置NUM_TOKENS_TO_GENERATE = 100  # 每个输入要生成的 token 数量TEMPERATURE = 0.8            # 温度参数(控制生成多样性)TOP_P = 0.8                  # Top‑p 采样阈值

这部分是针对香橙派的特别优化:

  • ON_ORANGE_PI是个开关,检测到在香橙派上运行时就会启用特殊配置

  • GRAPH_MODE和jit_level": "O2"是 MindSpore 的静态图模式和二级优化,能让模型跑得更快

  • 下面三个参数控制生成文本的长度和风格,数值不同生成效果也会不一样

3. 模型与分词器加载

model_id = "MindSpore-Lab/DeepSeek-R1-Distill-Qwen-1.5B-FP16"tokenizer = AutoTokenizer.from_pretrained(model_id, mirror="modelers")model = AutoModelForCausalLM.from_pretrained(    model_id,    ms_dtype=mindspore.float16,    low_cpu_mem_usage=True,    mirror="modelers",)# 使用 model.jit() 将全图静态图化model.jit()set_pyboost(False)print("[Init] 模型加载完成!")

和之前的代码相比有几个新东西:

  • low_cpu_mem_usage=True:这个参数很实用,能减少加载模型时对内存的占用,适合香橙派这种资源有限的设备

  • model.jit():把模型转换成静态图模式,虽然启动时会慢点,但运行起来速度更快

  • set_pyboost(False):关闭 MindSpore 的一个加速功能,可能在香橙派上更稳定

4. Top-p 采样函数

def sample_top_p(probs, p=0.9):    """Top-p采样函数,用于生成文本时选择下一个token"""    probs_np = probs.asnumpy()    # 按概率降序排序    sorted_indices = np.argsort(-probs_np, axis=-1)    sorted_probs = np.take_along_axis(probs_np, sorted_indices, axis=-1)    # 计算累积概率并创建掩码    cumulative_probs = np.cumsum(sorted_probs, axis=-1)    mask = cumulative_probs - sorted_probs > p    sorted_probs[mask] = 0.0    sorted_probs = sorted_probs / np.sum(sorted_probs, axis=-1, keepdims=True)    # 转换回 MindSpore Tensor    sorted_probs_tensor = mindspore.Tensor(sorted_probs, dtype=mindspore.float32)    sorted_indices_tensor = mindspore.Tensor(sorted_indices, dtype=mindspore.int32)    next_token_idx = ops.multinomial(sorted_probs_tensor, 1)    batch_size = probs.shape[0]    batch_indices = ops.arange(0, batch_size, dtype=mindspore.int32).reshape(-1, 1)    next_token = mindspore.ops.gather(sorted_indices_tensor, next_token_idx, axis=1, batch_dims=1)    return next_token

这个函数是用来从模型给出的多个可能的下一个词中做选择的:

  • 简单说就是先把可能的词按概率排序,然后只选那些累积概率不超过 TOP_P 的词

  • 这样既能保证生成的词比较合理,又能有一定的随机性

  • 注释里说用 numpy 实现是因为在香橙派上效率更高,看来是经过实际测试的

5. 解码函数

@mindspore.jit(jit_config=mindspore.JitConfig(jit_syntax_level='STRICT'))def get_decode_one_tokens_logits(model, cur_token, input_pos, cache_position, past_key_values, temperature=TEMPERATURE, top_p=TOP_P):    """单个token的解码函数,返回logits,可以使用jit进行优化"""    logits = model(        cur_token,        position_ids=input_pos,        cache_position=cache_position,        past_key_values=past_key_values,        return_dict=False,        use_cache=True    )[0]    return logitsdef decode_one_tokens(model, cur_token, input_pos, cache_position, past_key_values, temperature=TEMPERATURE, top_p=TOP_P):    """单个token的解码函数,由logits、温度和Top_p选择合适的token"""    logits = get_decode_one_tokens_logits(model, cur_token, input_pos, cache_position, past_key_values, temperature, top_p)    if temperature > 0:        probs = mindspore.mint.softmax(logits[:, -1] / temperature, dim=-1)        new_token = sample_top_p(probs, top_p)    else:        new_token = mindspore.mint.argmax(logits[:, -1], dim=-1)[:, None]    return new_token

这两个函数是生成单个词的核心:

  • @mindspore.jit装饰器能让函数编译成静态图,运行更快

  • get_decode_one_tokens_logits负责从模型获取每个可能的下一个词的概率

  • decode_one_tokens根据概率和前面设置的参数(温度、TOP_P)选择最合适的下一个词

6. 生成主函数

def generate_completion(prompt: str) -> str:    """核心推理函数:给定 prompt,生成补齐文本并返回"""    if not prompt:        return ""    inputs = tokenizer([prompt], return_tensors="ms", padding=True)    batch_size, seq_length = inputs["input_ids"].shape    # 创建静态缓存(用于加速自回归生成)    past_key_values = StaticCache(        config=model.config, max_batch_size=batch_size, max_cache_len=512, dtype=model.dtype    )    cache_position = ops.arange(seq_length)    generated_ids = ops.zeros(        batch_size, seq_length + NUM_TOKENS_TO_GENERATE + 1, dtype=mindspore.int32    )    generated_ids[:, cache_position] = inputs["input_ids"].to(mindspore.int32)    # 初始前向传播获取首个 logits    logits = model(        **inputs, cache_position=cache_position, past_key_values=past_key_values,        return_dict=False, use_cache=True    )[0]    # 生成第一个新 token    if TEMPERATURE > 0:        probs = mindspore.mint.softmax(logits[:, -1] / TEMPERATURE, dim=-1)        next_token = sample_top_p(probs, TOP_P)    else:        next_token = mindspore.mint.argmax(logits[:, -1], dim=-1)[:, None]    generated_ids[:, seq_length] = next_token[:, 0]    # 自回归生成循环    cache_position = mindspore.tensor([seq_length + 1])    partial_message = ""    for step in range(1, NUM_TOKENS_TO_GENERATE):        t_start = time.time()        next_token = decode_one_tokens(            model, next_token, None, cache_position, past_key_values,            temperature=TEMPERATURE, top_p=TOP_P        )        generated_ids[:, cache_position] = next_token.int()        cache_position += 1        t_elapsed = time.time() - t_start        print(f"[Token {step:02d}] {t_elapsed:.4f}s")  # 打印单步生成耗时        if tokenizer.eos_token_id in next_token.int()[0]:  # 遇到结束符就停止            break        new_word = tokenizer.decode(next_token.int()[0], skip_special_tokens=True)        partial_message += new_word        yield partial_message

这个函数是整个程序的 “发动机”:

  • 首先把用户输入的文字转换成模型能理解的格式

  • StaticCache是个好东西,能缓存之前计算的结果,避免重复计算,节省资源

  • 然后通过循环一个一个地生成词语(自回归生成)

  • 每次生成一个词都会计算耗时并打印出来,方便我们了解模型在香橙派上的运行速度

  • 用yield而不是return是为了能实时看到生成的内容,而不是等全部生成完

7. Gradio 界面设置

def gradio_predict(prompt: str) -> str:    """Gradio 回调:直接调用 generate_completion 并返回结果"""    return generate_completion(prompt)demo = gr.Interface(    fn=generate_completion,    inputs=gr.Textbox(lines=4, label="输入 Prompt", placeholder="请介绍一下自己。"),    outputs=gr.Textbox(lines=12, label="生成结果"),    title="DeepSeek‑R1‑Distill‑Qwen‑1.5B (使用MindSpore JIT推理优化)",    description=(        "基于MindSpore的DeepSeek‑R1‑Distill‑Qwen‑1.5B推理。生成每个token的耗时在终端进行显示。"    ),    examples=[        ["请介绍一下自己。"],        ["My favorite all time favorite condiment is ketchup."]    ],)if __name__ == "__main__":    demo.launch()

这部分和之前的代码类似,创建了一个网页界面:

  • 输入框让我们输入问题或提示

  • 输出框显示模型生成的回答

  • 还提供了几个示例问题,方便我们快速测试

总结

这段代码和上一段相比,主要是针对香橙派这种资源有限的设备做了很多优化:

  1. 启用了静态图模式和 JIT 优化,提高运行速度

  2. 增加了内存优化参数,减少资源占用

  3. 详细记录了每个词的生成时间,方便调试性能

  4. 优化了采样和生成过程,更适合在开发板上运行

1 个赞