File size: 8,690 Bytes
4e2a1b3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import torch, os, importlib, warnings, json, inspect
from typing import Dict, List, Tuple, Union
from ..core import ModelConfig, load_model
from ..core.device.npu_compatible_device import get_device_type
from ..utils.lora.merge import merge_lora


KVCache = Dict[str, Tuple[torch.Tensor, torch.Tensor]]


class TemplateModel(torch.nn.Module):
    def __init__(self):
        super().__init__()
    
    @torch.no_grad()
    def process_inputs(self, **kwargs):
        return {}
    
    def forward(self, **kwargs):
        raise NotImplementedError()


def check_template_model_format(model):
    if not hasattr(model, "process_inputs"):
        raise NotImplementedError("`process_inputs` is not implemented in the Template model.")
    if "kwargs" not in inspect.signature(model.process_inputs).parameters:
        raise NotImplementedError("`**kwargs` is not included in `process_inputs`.")
    if not hasattr(model, "forward"):
        raise NotImplementedError("`forward` is not implemented in the Template model.")
    if "kwargs" not in inspect.signature(model.forward).parameters:
        raise NotImplementedError("`**kwargs` is not included in `forward`.")


def load_template_model(path, torch_dtype=torch.bfloat16, device="cuda", verbose=1):
    spec = importlib.util.spec_from_file_location("template_model", os.path.join(path, "model.py"))
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    template_model_path = getattr(module, 'TEMPLATE_MODEL_PATH') if hasattr(module, 'TEMPLATE_MODEL_PATH') else None
    if template_model_path is not None:
        # With `TEMPLATE_MODEL_PATH`, a pretrained model will be loaded.
        model = load_model(
            model_class=getattr(module, 'TEMPLATE_MODEL'),
            config=getattr(module, 'TEMPLATE_MODEL_CONFIG') if hasattr(module, 'TEMPLATE_MODEL_CONFIG') else None,
            path=os.path.join(path, getattr(module, 'TEMPLATE_MODEL_PATH')),
            torch_dtype=torch_dtype,
            device=device,
        )
    else:
        # Without `TEMPLATE_MODEL_PATH`, a randomly initialized model or a non-model module will be loaded.
        model = module.TEMPLATE_MODEL()
        if hasattr(model, "to"):
            model = model.to(dtype=torch_dtype, device=device)
        if hasattr(model, "eval"):
            model = model.eval()
    check_template_model_format(model)
    if verbose > 0:
        metadata = {
            "model_architecture": getattr(module, 'TEMPLATE_MODEL').__name__,
            "code_path": os.path.join(path, "model.py"),
            "weight_path": template_model_path,
        }
        print(f"Template model loaded: {json.dumps(metadata, indent=4)}")
    return model


def load_template_data_processor(path):
    spec = importlib.util.spec_from_file_location("template_model", os.path.join(path, "model.py"))
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    if hasattr(module, 'TEMPLATE_DATA_PROCESSOR'):
        processor = getattr(module, 'TEMPLATE_DATA_PROCESSOR')
        return processor
    else:
        return None


class TemplatePipeline(torch.nn.Module):
    def __init__(
        self,
        torch_dtype: torch.dtype = torch.bfloat16,
        device: Union[str, torch.device] = get_device_type(),
        model_configs: list[ModelConfig] = [],
        lazy_loading: bool = False,
    ):
        super().__init__()
        self.torch_dtype = torch_dtype
        self.device = device
        self.model_configs = model_configs
        self.lazy_loading = lazy_loading
        if lazy_loading:
            for model_config in model_configs:
                TemplatePipeline.check_vram_config(model_config)
                model_config.download_if_necessary()
            self.models = None
        else:
            models = []
            for model_config in model_configs:
                TemplatePipeline.check_vram_config(model_config)
                model_config.download_if_necessary()
                model = load_template_model(model_config.path, torch_dtype=torch_dtype, device=device)
                models.append(model)
            self.models = torch.nn.ModuleList(models)

    def merge_kv_cache(self, kv_cache_list: List[KVCache]) -> KVCache:
        names = {}
        for kv_cache in kv_cache_list:
            for name in kv_cache:
                names[name] = None
        kv_cache_merged = {}
        for name in names:
            kv_list = [kv_cache.get(name) for kv_cache in kv_cache_list]
            kv_list = [kv for kv in kv_list if kv is not None]
            if len(kv_list) > 0:
                k = torch.concat([kv[0] for kv in kv_list], dim=1)
                v = torch.concat([kv[1] for kv in kv_list], dim=1)
                kv_cache_merged[name] = (k, v)
        return kv_cache_merged
    
    def merge_template_cache(self, template_cache_list):
        params = sorted(list(set(sum([list(template_cache.keys()) for template_cache in template_cache_list], []))))
        template_cache_merged = {}
        for param in params:
            data = [template_cache[param] for template_cache in template_cache_list if param in template_cache]
            if param == "kv_cache":
                data = self.merge_kv_cache(data)
            elif param == "lora":
                data = merge_lora(data)
            elif len(data) == 1:
                data = data[0]
            else:
                print(f"Conflict detected: `{param}` appears in the outputs of multiple Template models. Only the first one will be retained.")
                data = data[0]
            template_cache_merged[param] = data
        return template_cache_merged

    @staticmethod
    def check_vram_config(model_config: ModelConfig):
        params = [
            model_config.offload_device, model_config.offload_dtype,
            model_config.onload_device, model_config.onload_dtype,
            model_config.preparing_device, model_config.preparing_dtype,
            model_config.computation_device, model_config.computation_dtype,
        ]
        for param in params:
            if param is not None:
                warnings.warn("TemplatePipeline doesn't support VRAM management. VRAM config will be ignored.")

    @staticmethod
    def from_pretrained(
        torch_dtype: torch.dtype = torch.bfloat16,
        device: Union[str, torch.device] = get_device_type(),
        model_configs: list[ModelConfig] = [],
        lazy_loading: bool = False,
    ):
        pipe = TemplatePipeline(torch_dtype, device, model_configs, lazy_loading)
        return pipe
    
    def fetch_model(self, model_id):
        if self.lazy_loading:
            model_config = self.model_configs[model_id]
            model_config.download_if_necessary()
            model = load_template_model(model_config.path, torch_dtype=self.torch_dtype, device=self.device)
        else:
            model = self.models[model_id]
        return model
    
    def call_single_side(self, pipe=None, inputs: List[Dict] = None):
        model = None
        onload_model_id = -1
        template_cache = []
        for i in inputs:
            model_id = i.get("model_id", 0)
            if model_id != onload_model_id:
                model = self.fetch_model(model_id)
                onload_model_id = model_id
            cache = model.process_inputs(pipe=pipe, **i)
            cache = model.forward(pipe=pipe, **cache)
            template_cache.append(cache)
        template_cache = self.merge_template_cache(template_cache)
        return template_cache
        
    @torch.no_grad()
    def __call__(
        self,
        pipe=None,
        template_inputs: List[Dict] = None,
        negative_template_inputs: List[Dict] = None,
        **kwargs,
    ):
        template_cache = self.call_single_side(pipe=pipe, inputs=template_inputs or [])
        negative_template_cache = self.call_single_side(pipe=pipe, inputs=negative_template_inputs or [])
        required_params = list(inspect.signature(pipe.__call__).parameters.keys())
        for param in template_cache:
            if param in required_params:
                kwargs[param] = template_cache[param]
            else:
                print(f"`{param}` is not included in the inputs of `{pipe.__class__.__name__}`. This parameter will be ignored.")
        for param in negative_template_cache:
            if "negative_" + param in required_params:
                kwargs["negative_" + param] = negative_template_cache[param]
            else:
                print(f"`{'negative_' + param}` is not included in the inputs of `{pipe.__class__.__name__}`. This parameter will be ignored.")
        return pipe(**kwargs)