b-re-w commited on
Commit
3e04ca0
ยท
verified ยท
1 Parent(s): a6bb111

Upload merge.py

Browse files
Files changed (1) hide show
  1. merge.py +375 -0
merge.py ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.distributed.tensor
3
+ from safetensors.torch import save_file
4
+ import os
5
+ from collections import OrderedDict
6
+ import gc
7
+
8
+ def merge_fsdp_to_safetensors(rank0_path, rank1_path, output_path, target_dtype=None):
9
+ """
10
+ FSDP๋กœ ๋ถ„ํ• ๋œ ๋‘ ๊ฐœ์˜ .pt ํŒŒ์ผ์„ ํ•˜๋‚˜์˜ .safetensors ํŒŒ์ผ๋กœ ๋ณ‘ํ•ฉ
11
+
12
+ Args:
13
+ rank0_path (str): rank 0 .pt ํŒŒ์ผ ๊ฒฝ๋กœ
14
+ rank1_path (str): rank 1 .pt ํŒŒ์ผ ๊ฒฝ๋กœ
15
+ output_path (str): ์ถœ๋ ฅํ•  .safetensors ํŒŒ์ผ ๊ฒฝ๋กœ
16
+ target_dtype (torch.dtype, optional): ํƒ€๊ฒŸ dtype (์˜ˆ: torch.float16, torch.bfloat16)
17
+ """
18
+ print("Loading rank 0 checkpoint...")
19
+ rank0_dict = torch.load(rank0_path, map_location='cpu', weights_only=False)
20
+
21
+ print("Loading rank 1 checkpoint...")
22
+ rank1_dict = torch.load(rank1_path, map_location='cpu', weights_only=False)
23
+
24
+ # DTensor๋ฅผ ์ผ๋ฐ˜ ํ…์„œ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜
25
+ def convert_dtensor_to_tensor(state_dict):
26
+ converted_dict = OrderedDict()
27
+ dtype_info = {}
28
+ for key, value in state_dict.items():
29
+ if hasattr(value, '_local_tensor'):
30
+ # DTensor์ธ ๊ฒฝ์šฐ ๋กœ์ปฌ ํ…์„œ ์ถ”์ถœ
31
+ tensor = value._local_tensor
32
+ converted_dict[key] = tensor
33
+ dtype_info[key] = tensor.dtype
34
+ print(f"Converted DTensor to tensor: {key} (dtype: {tensor.dtype})")
35
+ elif isinstance(value, torch.Tensor):
36
+ converted_dict[key] = value
37
+ dtype_info[key] = value.dtype
38
+ else:
39
+ # ๋‹ค๋ฅธ ํƒ€์ž…์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€
40
+ converted_dict[key] = value
41
+ dtype_info[key] = type(value).__name__
42
+ return converted_dict, dtype_info
43
+
44
+ print("Converting DTensors to regular tensors...")
45
+ rank0_dict, rank0_dtypes = convert_dtensor_to_tensor(rank0_dict)
46
+ rank1_dict, rank1_dtypes = convert_dtensor_to_tensor(rank1_dict)
47
+
48
+ # dtype ์ •๋ณด ์ถœ๋ ฅ
49
+ print("\n๐Ÿ“‹ Original dtype information:")
50
+ all_dtypes_r0 = set(dtype_info for dtype_info in rank0_dtypes.values() if isinstance(dtype_info, torch.dtype))
51
+ all_dtypes_r1 = set(dtype_info for dtype_info in rank1_dtypes.values() if isinstance(dtype_info, torch.dtype))
52
+ all_dtypes = all_dtypes_r0 | all_dtypes_r1
53
+
54
+ print(f" Rank 0 dtypes found: {all_dtypes_r0}")
55
+ print(f" Rank 1 dtypes found: {all_dtypes_r1}")
56
+ print(f" All dtypes: {all_dtypes}")
57
+
58
+ if target_dtype:
59
+ print(f" Target dtype specified: {target_dtype}")
60
+ else:
61
+ print(" No target dtype specified - keeping original dtypes")
62
+
63
+ # ๋ณ‘ํ•ฉ๋œ ์ƒํƒœ ์‚ฌ์ „
64
+ merged_state_dict = OrderedDict()
65
+
66
+ # rank 0์˜ ๋ชจ๋“  ํ‚ค๋“ค์„ ๋จผ์ € ์ฒ˜๋ฆฌ
67
+ all_keys = set(rank0_dict.keys()) | set(rank1_dict.keys())
68
+
69
+ print(f"Total unique keys found: {len(all_keys)}")
70
+
71
+ for key in sorted(all_keys):
72
+ rank0_tensor = rank0_dict.get(key)
73
+ rank1_tensor = rank1_dict.get(key)
74
+
75
+ if rank0_tensor is not None and rank1_tensor is not None:
76
+ # ๋‘ rank์— ๋ชจ๋‘ ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ - ์ฐจ์› ํ™•์ธ ํ›„ ์—ฐ๊ฒฐ
77
+ print(f"Merging key: {key}")
78
+ print(f" Rank 0 shape: {rank0_tensor.shape}, dtype: {rank0_tensor.dtype}")
79
+ print(f" Rank 1 shape: {rank1_tensor.shape}, dtype: {rank1_tensor.dtype}")
80
+
81
+ # dtype ๋ณ€ํ™˜ (ํ•„์š”ํ•œ ๊ฒฝ์šฐ)
82
+ if target_dtype and rank0_tensor.dtype != target_dtype:
83
+ rank0_tensor = rank0_tensor.to(target_dtype)
84
+ print(f" Converted rank 0 to {target_dtype}")
85
+ if target_dtype and rank1_tensor.dtype != target_dtype:
86
+ rank1_tensor = rank1_tensor.to(target_dtype)
87
+ print(f" Converted rank 1 to {target_dtype}")
88
+
89
+ # ์ฒซ ๋ฒˆ์งธ ์ฐจ์›์œผ๋กœ ์—ฐ๊ฒฐ (์ผ๋ฐ˜์ ์ธ FSDP ์ƒค๋”ฉ ๋ฐฉ์‹)
90
+ merged_tensor = torch.cat([rank0_tensor, rank1_tensor], dim=0)
91
+ merged_state_dict[key] = merged_tensor
92
+ print(f" Merged shape: {merged_tensor.shape}, dtype: {merged_tensor.dtype}")
93
+
94
+ elif rank0_tensor is not None:
95
+ # rank 0์—๋งŒ ์กด์žฌ
96
+ tensor = rank0_tensor
97
+ if target_dtype and isinstance(tensor, torch.Tensor) and tensor.dtype != target_dtype:
98
+ tensor = tensor.to(target_dtype)
99
+ print(f"Converting {key} from rank 0: {rank0_tensor.dtype} -> {target_dtype}")
100
+ print(f"Adding from rank 0: {key} (shape: {tensor.shape if isinstance(tensor, torch.Tensor) else 'N/A'}, dtype: {tensor.dtype if isinstance(tensor, torch.Tensor) else type(tensor).__name__})")
101
+ merged_state_dict[key] = tensor
102
+
103
+ elif rank1_tensor is not None:
104
+ # rank 1์—๋งŒ ์กด์žฌ
105
+ tensor = rank1_tensor
106
+ if target_dtype and isinstance(tensor, torch.Tensor) and tensor.dtype != target_dtype:
107
+ tensor = tensor.to(target_dtype)
108
+ print(f"Converting {key} from rank 1: {rank1_tensor.dtype} -> {target_dtype}")
109
+ print(f"Adding from rank 1: {key} (shape: {tensor.shape if isinstance(tensor, torch.Tensor) else 'N/A'}, dtype: {tensor.dtype if isinstance(tensor, torch.Tensor) else type(tensor).__name__})")
110
+ merged_state_dict[key] = tensor
111
+
112
+ print(f"\nTotal merged parameters: {len(merged_state_dict)}")
113
+
114
+ # ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ
115
+ del rank0_dict, rank1_dict
116
+ gc.collect()
117
+
118
+ # safetensors๋กœ ์ €์žฅ
119
+ print(f"Saving merged model to {output_path}...")
120
+
121
+ # ์ตœ์ข… dtype ์ •๋ณด ์ถœ๋ ฅ
122
+ final_dtypes = {}
123
+ for key, tensor in merged_state_dict.items():
124
+ if isinstance(tensor, torch.Tensor):
125
+ final_dtypes[tensor.dtype] = final_dtypes.get(tensor.dtype, 0) + 1
126
+
127
+ print(f"๐Ÿ“‹ Final merged model dtype distribution:")
128
+ for dtype, count in final_dtypes.items():
129
+ print(f" {dtype}: {count} tensors")
130
+
131
+ save_file(merged_state_dict, output_path)
132
+ print("โœ… Successfully saved merged model!")
133
+
134
+ return merged_state_dict
135
+
136
+ def merge_with_custom_concatenation(rank0_path, rank1_path, output_path, concat_rules=None):
137
+ """
138
+ ์‚ฌ์šฉ์ž ์ •์˜ ์—ฐ๊ฒฐ ๊ทœ์น™์œผ๋กœ ๋ณ‘ํ•ฉ
139
+
140
+ Args:
141
+ concat_rules (dict): ํ‚ค๋ณ„ ์—ฐ๊ฒฐ ์ฐจ์› ์ง€์ • {'key_pattern': dim}
142
+ """
143
+ if concat_rules is None:
144
+ # ๊ธฐ๋ณธ ๊ทœ์น™
145
+ concat_rules = {
146
+ 'weight': 0, # ๊ฐ€์ค‘์น˜๋Š” ์ฒซ ๋ฒˆ์งธ ์ฐจ์›์œผ๋กœ ์—ฐ๊ฒฐ
147
+ 'bias': 0, # ํŽธํ–ฅ๋„ ์ฒซ ๋ฒˆ์งธ ์ฐจ์›์œผ๋กœ ์—ฐ๊ฒฐ
148
+ }
149
+
150
+ print("Loading checkpoints...")
151
+ rank0_dict = torch.load(rank0_path, map_location='cpu', weights_only=False)
152
+ rank1_dict = torch.load(rank1_path, map_location='cpu', weights_only=False)
153
+
154
+ merged_state_dict = OrderedDict()
155
+ all_keys = set(rank0_dict.keys()) | set(rank1_dict.keys())
156
+
157
+ for key in sorted(all_keys):
158
+ rank0_tensor = rank0_dict.get(key)
159
+ rank1_tensor = rank1_dict.get(key)
160
+
161
+ if rank0_tensor is not None and rank1_tensor is not None:
162
+ # ์—ฐ๊ฒฐ ์ฐจ์› ๊ฒฐ์ •
163
+ concat_dim = 0 # ๊ธฐ๋ณธ๊ฐ’
164
+ for pattern, dim in concat_rules.items():
165
+ if pattern in key:
166
+ concat_dim = dim
167
+ break
168
+
169
+ print(f"Merging {key} along dimension {concat_dim}")
170
+ merged_tensor = torch.cat([rank0_tensor, rank1_tensor], dim=concat_dim)
171
+ merged_state_dict[key] = merged_tensor
172
+
173
+ elif rank0_tensor is not None:
174
+ merged_state_dict[key] = rank0_tensor
175
+ elif rank1_tensor is not None:
176
+ merged_state_dict[key] = rank1_tensor
177
+
178
+ # ์ •๋ฆฌ ๋ฐ ์ €์žฅ
179
+ del rank0_dict, rank1_dict
180
+ gc.collect()
181
+
182
+ print(f"Saving to {output_path}...")
183
+ save_file(merged_state_dict, output_path)
184
+ print("โœ… Merge completed!")
185
+
186
+ def comprehensive_verification(rank0_path, rank1_path, merged_path):
187
+ """๋ณ‘ํ•ฉ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋˜์—ˆ๋Š”์ง€ ์ข…ํ•ฉ์ ์œผ๋กœ ๊ฒ€์ฆ"""
188
+ import torch.distributed.tensor
189
+ from safetensors import safe_open
190
+
191
+ print("๐Ÿ” Starting comprehensive verification...")
192
+
193
+ # 1. ์›๋ณธ ํŒŒ์ผ๋“ค ๋กœ๋“œ
194
+ print("\n๐Ÿ“ Loading original files...")
195
+ rank0_dict = torch.load(rank0_path, map_location='cpu', weights_only=False)
196
+ rank1_dict = torch.load(rank1_path, map_location='cpu', weights_only=False)
197
+
198
+ # DTensor๋ฅผ ์ผ๋ฐ˜ ํ…์„œ๋กœ ๋ณ€ํ™˜
199
+ def convert_dtensor_to_tensor(state_dict):
200
+ converted_dict = {}
201
+ for key, value in state_dict.items():
202
+ if hasattr(value, '_local_tensor'):
203
+ converted_dict[key] = value._local_tensor
204
+ elif isinstance(value, torch.Tensor):
205
+ converted_dict[key] = value
206
+ else:
207
+ converted_dict[key] = value
208
+ return converted_dict
209
+
210
+ rank0_dict = convert_dtensor_to_tensor(rank0_dict)
211
+ rank1_dict = convert_dtensor_to_tensor(rank1_dict)
212
+
213
+ # 2. ์›๋ณธ ํŒŒ์ผ๋“ค ๋ถ„์„
214
+ rank0_keys = set(rank0_dict.keys())
215
+ rank1_keys = set(rank1_dict.keys())
216
+ all_original_keys = rank0_keys | rank1_keys
217
+ shared_keys = rank0_keys & rank1_keys
218
+ rank0_only = rank0_keys - rank1_keys
219
+ rank1_only = rank1_keys - rank0_keys
220
+
221
+ print(f"๐Ÿ“Š Original files analysis:")
222
+ print(f" Rank 0 keys: {len(rank0_keys)}")
223
+ print(f" Rank 1 keys: {len(rank1_keys)}")
224
+ print(f" Shared keys: {len(shared_keys)}")
225
+ print(f" Rank 0 only: {len(rank0_only)}")
226
+ print(f" Rank 1 only: {len(rank1_only)}")
227
+ print(f" Total unique keys: {len(all_original_keys)}")
228
+
229
+ # 3. ์›๋ณธ ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆ˜ ๊ณ„์‚ฐ
230
+ original_params = 0
231
+ original_shapes = {}
232
+
233
+ for key in all_original_keys:
234
+ if key in shared_keys:
235
+ # ๊ณต์œ  ํ‚ค๋Š” ๋‘ ํ…์„œ๋ฅผ ์—ฐ๊ฒฐํ•œ ํฌ๊ธฐ๋กœ ๊ณ„์‚ฐ
236
+ r0_tensor = rank0_dict[key]
237
+ r1_tensor = rank1_dict[key]
238
+ combined_shape = list(r0_tensor.shape)
239
+ combined_shape[0] += r1_tensor.shape[0] # ์ฒซ ๋ฒˆ์งธ ์ฐจ์›์œผ๋กœ ์—ฐ๊ฒฐ ๊ฐ€์ •
240
+ original_shapes[key] = tuple(combined_shape)
241
+ original_params += r0_tensor.numel() + r1_tensor.numel()
242
+ elif key in rank0_only:
243
+ original_shapes[key] = rank0_dict[key].shape
244
+ original_params += rank0_dict[key].numel()
245
+ elif key in rank1_only:
246
+ original_shapes[key] = rank1_dict[key].shape
247
+ original_params += rank1_dict[key].numel()
248
+
249
+ print(f" Original total parameters: {original_params:,}")
250
+
251
+ # 4. ๋ณ‘ํ•ฉ๋œ ํŒŒ์ผ ๋ถ„์„
252
+ print(f"\n๐Ÿ“ Loading merged file: {merged_path}")
253
+ merged_params = 0
254
+ merged_keys = set()
255
+ merged_shapes = {}
256
+
257
+ with safe_open(merged_path, framework="pt", device="cpu") as f:
258
+ merged_keys = set(f.keys())
259
+ for key in f.keys():
260
+ tensor = f.get_tensor(key)
261
+ merged_shapes[key] = tensor.shape
262
+ merged_params += tensor.numel()
263
+
264
+ print(f"๐Ÿ“Š Merged file analysis:")
265
+ print(f" Merged keys: {len(merged_keys)}")
266
+ print(f" Merged parameters: {merged_params:,}")
267
+
268
+ # 5. ๋น„๊ต ๋ฐ ๊ฒ€์ฆ
269
+ print(f"\nโœ… Verification Results:")
270
+
271
+ # ํ‚ค ๊ฐœ์ˆ˜ ๋น„๊ต
272
+ keys_match = len(merged_keys) == len(all_original_keys)
273
+ print(f" Keys count match: {keys_match} ({len(merged_keys)} vs {len(all_original_keys)})")
274
+
275
+ # ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆ˜ ๋น„๊ต
276
+ params_match = merged_params == original_params
277
+ print(f" Parameter count match: {params_match} ({merged_params:,} vs {original_params:,})")
278
+
279
+ # ํ‚ค ์ด๋ฆ„ ๋น„๊ต
280
+ missing_keys = all_original_keys - merged_keys
281
+ extra_keys = merged_keys - all_original_keys
282
+
283
+ if missing_keys:
284
+ print(f" โŒ Missing keys: {missing_keys}")
285
+
286
+ if extra_keys:
287
+ print(f" โŒ Extra keys: {extra_keys}")
288
+
289
+ # ๊ฐœ๋ณ„ ํ…์„œ ํฌ๊ธฐ ๋น„๊ต
290
+ shape_mismatches = []
291
+ for key in merged_keys & all_original_keys:
292
+ if merged_shapes[key] != original_shapes[key]:
293
+ shape_mismatches.append((key, merged_shapes[key], original_shapes[key]))
294
+
295
+ if shape_mismatches:
296
+ print(f" โŒ Shape mismatches:")
297
+ for key, merged_shape, original_shape in shape_mismatches[:5]: # ์ฒ˜์Œ 5๊ฐœ๋งŒ ํ‘œ์‹œ
298
+ print(f" {key}: {merged_shape} vs {original_shape}")
299
+ if len(shape_mismatches) > 5:
300
+ print(f" ... and {len(shape_mismatches) - 5} more")
301
+
302
+ # 6. ์„ธ๋ถ€ ๋ถ„์„ (์„ ํƒ์ )
303
+ print(f"\n๐Ÿ“‹ Detailed Analysis:")
304
+ print(f" Shared keys that should be concatenated:")
305
+ for key in sorted(list(shared_keys))[:10]: # ์ฒ˜์Œ 10๊ฐœ๋งŒ ํ‘œ์‹œ
306
+ r0_shape = rank0_dict[key].shape
307
+ r1_shape = rank1_dict[key].shape
308
+ expected_shape = list(r0_shape)
309
+ expected_shape[0] += r1_shape[0]
310
+ actual_shape = merged_shapes.get(key, "MISSING")
311
+ status = "โœ…" if tuple(expected_shape) == actual_shape else "โŒ"
312
+ print(f" {status} {key}: {r0_shape} + {r1_shape} -> {actual_shape}")
313
+
314
+ if len(shared_keys) > 10:
315
+ print(f" ... and {len(shared_keys) - 10} more shared keys")
316
+
317
+ # 7. ์ตœ์ข… ๊ฒฐ๊ณผ
318
+ overall_success = keys_match and params_match and not missing_keys and not extra_keys and not shape_mismatches
319
+
320
+ print(f"\n{'='*50}")
321
+ if overall_success:
322
+ print("๐ŸŽ‰ MERGE VERIFICATION SUCCESSFUL!")
323
+ print(" All parameters have been correctly merged.")
324
+ else:
325
+ print("โš ๏ธ MERGE VERIFICATION FOUND ISSUES!")
326
+ print(" Please review the mismatches above.")
327
+ print(f"{'='*50}")
328
+
329
+ # ์ •๋ฆฌ
330
+ del rank0_dict, rank1_dict
331
+ gc.collect()
332
+
333
+ return overall_success
334
+
335
+ # ์‚ฌ์šฉ ์˜ˆ์‹œ
336
+ if __name__ == "__main__":
337
+ # ํŒŒ์ผ ๊ฒฝ๋กœ ์„ค์ •
338
+ rank0_file = "model_rank_0.pt" # ์‹ค์ œ ํŒŒ์ผ๋ช…์œผ๋กœ ๋ณ€๊ฒฝ
339
+ rank1_file = "model_rank_1.pt" # ์‹ค์ œ ํŒŒ์ผ๋ช…์œผ๋กœ ๋ณ€๊ฒฝ
340
+ output_file = "merged_model.safetensors"
341
+
342
+ # dtype ์˜ต์…˜ ์„ค์ •
343
+ target_dtype = torch.bfloat16 # bf16์œผ๋กœ ๋ณ€ํ™˜
344
+
345
+ # ๊ธฐ๋ณธ ๋ณ‘ํ•ฉ
346
+ print("Starting merge process...")
347
+ merged_dict = merge_fsdp_to_safetensors(rank0_file, rank1_file, output_file, target_dtype)
348
+
349
+ # ์ข…ํ•ฉ์ ์ธ ๊ฒ€์ฆ
350
+ print("\nStarting comprehensive verification...")
351
+ verification_passed = comprehensive_verification(rank0_file, rank1_file, output_file)
352
+
353
+ if verification_passed:
354
+ print(f"\n๐ŸŽ‰ Successfully merged and verified FSDP model to {output_file}")
355
+ else:
356
+ print(f"\nโš ๏ธ Merge completed but verification found issues. Please review the output above.")
357
+
358
+ # ์ถ”๊ฐ€: ๊ฐ„๋‹จํ•œ ๋กœ๋“œ ํ…Œ์ŠคํŠธ
359
+ print(f"\n๐Ÿ” Testing if merged model can be loaded...")
360
+ try:
361
+ from safetensors import safe_open
362
+ with safe_open(output_file, framework="pt", device="cpu") as f:
363
+ sample_keys = list(f.keys())[:3]
364
+ for key in sample_keys:
365
+ tensor = f.get_tensor(key)
366
+ print(f" โœ… Successfully loaded {key}: {tensor.shape}, dtype: {tensor.dtype}")
367
+ print(" โœ… Merged model loads correctly!")
368
+ except Exception as e:
369
+ print(f" โŒ Error loading merged model: {e}")
370
+
371
+ print(f"\n๐Ÿ’ก Tip: To change dtype, modify 'target_dtype' in the script:")
372
+ print(f" - torch.float16 for fp16 (smaller file, less precision)")
373
+ print(f" - torch.bfloat16 for bf16 (good balance)")
374
+ print(f" - torch.float32 for fp32 (larger file, best precision)")
375
+ print(f" - None to keep original dtypes")