File size: 11,876 Bytes
5e27996
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
import sys
import os
from typing import List, Dict
from dataclasses import dataclass
import multiprocessing as mp
import pathlib
import torch

from transformers import AutoTokenizer, BitsAndBytesConfig

project_path = pathlib.Path(__file__).parent.parent
sys.path.append(str(project_path))

from src.utils.gpu_worker import GpuWorker
from src.utils.template import QWEN3_TEMPLATE, QWEN3_INSTRUCT_TEMPLATE
from src.utils.cache import  copy_kv_cache_to_device, CustomDynamicCacheOnCPU
from src.msa.model import MSAForCausalLM
from src.utils.gpu_worker import GpuWorker
from src.utils.tools import compose_input
from src.types import Document, ProtocolConstants

@dataclass
class BlockModelInput:
    doc_input_ids: torch.Tensor
    doc_attention_mask: torch.Tensor
    doc_ids: torch.Tensor
    position_ids: torch.Tensor
    num_chunks: int
    chunk_sizes: List[int]

PREFILL_WORKER_READY = "PREFILL_WORKER_READY"
PREFILL_WORKER_CLOSE = "PREFILL_WORKER_CLOSE"
PREFILL_WORKER_MEMORY_DOCS = "PREFILL_WORKER_MEMORY_DOCS"
PREFILL_WORKER_NUM_CHUNKS_REPORT = "PREFILL_WORKER_NUM_CHUNKS_REPORT"
PREFILL_WORKER_META = "PREFILL_WORKER_META"

class PrefillStage1Worker(GpuWorker):
    """Memory工作进程"""

    def __init__(self, gpu_id: int, model_path: str,  template: dict,
                 pooling_kernel_size: int, envs: dict):
        """
        该 worker 被MemoryWorker创建并仅执行prefill stage 1获取block 的kv cache
        """
        super().__init__(gpu_id, envs)
        self.model_path = model_path
        self.pooling_kernel_size = pooling_kernel_size

        self._load_model()
        self.model_config = self.model.config

        self.template_id = -2
        self._prepare_template(template)

    def num_model_layers(self):
        return self.model_config.num_hidden_layers

    def _load_model(self):
        """加载模型和tokenizer"""
        # 加载tokenizer
        self.tokenizer = AutoTokenizer.from_pretrained(self.model_path)

        # 加载模型
        self.model = MSAForCausalLM.from_pretrained(
            self.model_path,
            use_cache=True,
            attn_implementation="flash_attention_2",
            torch_dtype="bfloat16",
            device_map=self.device,
        )
        self.model.eval()

    
    @staticmethod
    def split_docs(docs: List[Document], block_size: int):
        sub_blocks = []
        curr_block = []
        sz = 0
        for doc in docs:
            chunks = doc.num_chunks
            if sz + chunks > block_size and curr_block:
                sub_blocks.append(curr_block)
                sz = 0
                curr_block = []
            curr_block.append(doc)
            sz += chunks
        
        if curr_block:
            sub_blocks.append(curr_block)
        
        return sub_blocks
    
    @staticmethod
    def wait_for_ready(q: mp.Queue):               
        ProtocolConstants.expect(q, PREFILL_WORKER_READY)

    @staticmethod
    def close_worker(q: mp.Queue):               
        ProtocolConstants.send(q, PREFILL_WORKER_CLOSE, block=True)

    @staticmethod
    def send_documents(q: mp.Queue, docs):
        ProtocolConstants.send(q,
                               PREFILL_WORKER_MEMORY_DOCS,
                               data=docs,
                               block=False)
    @staticmethod
    def recv_meta(q: mp.Queue):
        return ProtocolConstants.expect(q, PREFILL_WORKER_META)

    @staticmethod
    def prefill_worker_main(gpu_id: int, request_queue: mp.Queue, response_queue: mp.Queue,
                            model_path: str, template: Dict,
                            pooling_kernel_size: int, block_size: int, envs):
        
        # print(f"prefill worker {gpu_id} started")
        worker = PrefillStage1Worker(gpu_id, model_path, template, pooling_kernel_size, envs)

        # notify parent I'm ready
        ProtocolConstants.send(response_queue, PREFILL_WORKER_READY, block=False)

        docs: List[Document] = ProtocolConstants.expect(request_queue, PREFILL_WORKER_MEMORY_DOCS)
        try:
            for block in PrefillStage1Worker.split_docs(docs, block_size):
                meta = worker.inference(block)

                # send to master worker process and continue, DO NOT block
                ProtocolConstants.send(response_queue, 
                                    PREFILL_WORKER_META,
                                    data=meta,
                                    block=False)

        except Exception as e:
            print(f"[子进程 {gpu_id}] 发生错误: {e}")
            import traceback
            traceback.print_exc()
        
        # wait for exit signal
        ProtocolConstants.expect(request_queue, PREFILL_WORKER_CLOSE)
        print(f"prefill worker {gpu_id} ended")

    def inference(self, block: List[Document]):
        """
        处理memory block

        Args:
            memory_block: 分配给此GPU的memory block

        """
        model_input = self._prepare_block_inputs(block)

        kv_meta = self._inference(model_input)
        kv_meta['nr_docs'] = len(block)
        kv_meta['doc_ids'] = [item.doc_id for item in block]
        kv_meta['nr_chunks'] = [item.num_chunks for item in block]

        return kv_meta

    def _prepare_template(self, template: Dict) -> Dict:
        """
        重新加载单个memory block
        完整复制eval_anything_v2_batch.py中reload_memory的逻辑

        Args:
            block: memory block数据 [(doc_id, doc_str), ...]
            template: 模板字典

        Returns:
            KV cache元数据
        """
        # 获取模板信息
        self.pad_token = self.tokenizer.pad_token
        self.pad_token_id = self.tokenizer.pad_token_id
        self.doc_end_id = self.tokenizer("<|im_end|>", add_special_tokens=False)["input_ids"]

        prompt_template = template["prompt"].replace("{prompt}", self.pad_token)
        prompt_template_inputs = self.tokenizer(prompt_template, add_special_tokens=False)
        self.prompt_template_input_ids = prompt_template_inputs["input_ids"]
        self.prompt_template_attention_mask = prompt_template_inputs["attention_mask"]

        self.pad_index = self.prompt_template_input_ids.index(self.pad_token_id)
        self.template_head_input_ids = self.prompt_template_input_ids[:self.pad_index]
        self.template_head_attention_mask = self.prompt_template_attention_mask[:self.pad_index]
        self.template_tail_input_ids = self.prompt_template_input_ids[self.pad_index+1:]
        self.template_tail_attention_mask = self.prompt_template_attention_mask[self.pad_index+1:]


    def _prepare_block_inputs(self, block: List[Document]) -> Dict:
        """
        重新加载单个memory block
        完整复制eval_anything_v2_batch.py中reload_memory的逻辑

        Args:
            block: memory block数据 [(doc_id, doc_str), ...]
            template: 模板字典

        Returns:
            KV cache元数据
        """

        # 准备文档数据

        # block starts with template head
        doc_ids = [self.template_id] * len(self.template_head_input_ids)
        doc_input_ids = [i for i in self.template_head_input_ids ]
        doc_attention_mask = [i for i in self.template_head_attention_mask]
        position_ids = [i for i in range(self.pad_index)]

        chunk_sizes = [] # 记录每一份文档占用的 chunk 数量

        for doc_idx, item in enumerate(block):
            doc_id, doc, pre_calculated_num_chunk = item.doc_id, item.doc, item.num_chunks
            new_doc, doc_inputs = compose_input(doc, doc_id, self.tokenizer)
            # print(f"inference {doc_id+1}: {new_doc}")
            # 此处必须使用 1 起始的doc_idx,因为此 id 是用于生成 pool doc ID 的
            # 注意不可以使用doc_id,doc_id只能用于嵌入在语料中,使得 generate 时能生成出来,
            # 而pool doc ID的作用却是用于标注产生的 kv cache chunks 和文档的对应关系
            # 所以每次 stage1 的推理doc id 都是从 1 开始的
            temp_doc_ids = [doc_idx+1] * len(doc_inputs["input_ids"])
            temp_doc_input_ids = doc_inputs["input_ids"]
            temp_doc_attention_mask = doc_inputs["attention_mask"]
            length = len(temp_doc_input_ids)
            temp_position_ids = [i for i in range(length)]

            chunk_size = (len(temp_doc_ids) + self.pooling_kernel_size - 1) // self.pooling_kernel_size
            chunk_sizes.append(chunk_size)
            assert chunk_size == pre_calculated_num_chunk, f"pre calculated chunk {pre_calculated_num_chunk} got {chunk_size}, doc str {len(doc)} id len {length}/{len(temp_doc_ids)}: [{doc_id}] <{doc}>"



            doc_ids.extend(temp_doc_ids)
            doc_input_ids.extend(temp_doc_input_ids)
            doc_attention_mask.extend(temp_doc_attention_mask)
            position_ids.extend(temp_position_ids)

        input_ids_tensor = torch.LongTensor([doc_input_ids])
        attention_mask_tensor = torch.LongTensor([doc_attention_mask])
        doc_ids_tensor = torch.LongTensor([doc_ids])
        position_ids_tensor = torch.LongTensor([position_ids])

        return BlockModelInput(doc_input_ids=input_ids_tensor,
                               doc_attention_mask=attention_mask_tensor,
                               doc_ids=doc_ids_tensor,
                               position_ids=position_ids_tensor,
                               num_chunks=sum(chunk_sizes),
                               chunk_sizes=chunk_sizes)

    def _inference(self, model_input: BlockModelInput) -> Dict:
        """
        重新加载单个memory block
        完整复制eval_anything_v2_batch.py中reload_memory的逻辑

        Args:
            block: memory block数据 [(doc_id, doc_str), ...]
            template: 模板字典

        Returns:
            KV cache元数据
        """

        # 转换为tensor
        input_ids_tensor = model_input.doc_input_ids.to(self.device)
        attention_mask_tensor = model_input.doc_attention_mask.to(self.device)
        doc_ids_tensor = model_input.doc_ids.to(self.device)
        position_ids_tensor = model_input.position_ids.to(self.device)

        # 创建past_key_values,这里我们会直接将 kvcache 和其他的 tensor 全部保存在 cpu 上
        past_key_values = CustomDynamicCacheOnCPU()
        for layer_idx in range(self.num_model_layers()):
            past_key_values.record_kwargs(layer_idx, {"stage": "prefill_stage1"})

        # 执行prefill
        # TODO: 多 batch 会更快
        with torch.no_grad():
            if True:
                """我们的数据太大了,会导致model lm_head产生大量的显存堆积,所以直接用model.model避过去"""
                outputs = self.model.model(
                    input_ids=input_ids_tensor,
                    attention_mask=attention_mask_tensor,
                    position_ids=position_ids_tensor,
                    past_key_values=past_key_values,
                    use_cache=True,
                    output_attentions=False,
                    output_hidden_states=False,
                    output_docs_score=False,
                    doc_ids=doc_ids_tensor,
                )
            else:
                outputs = self.model(
                    input_ids=input_ids_tensor,
                    attention_mask=attention_mask_tensor,
                    doc_ids=doc_ids_tensor,
                    use_cache=True,
                    position_ids=position_ids_tensor,
                    past_key_values=past_key_values,
                )

        torch.cuda.empty_cache()
        # 构建返回的元数据
        kvcache_meta = {
            "chunk_sizes": model_input.chunk_sizes,
            "past_key_values": outputs.past_key_values,
        }

        return kvcache_meta