|
36 | 36 | from .generate import ( |
37 | 37 | BatchGenerator, |
38 | 38 | SequenceStateMachine, |
39 | | - generation_stream, |
40 | 39 | stream_generate, |
41 | 40 | ) |
42 | 41 | from .models.cache import ( |
@@ -279,8 +278,7 @@ def __next__(self): |
279 | 278 | self._loops += 1 |
280 | 279 | self._time_spent += time.time() - self._start |
281 | 280 | if self._loops % self._sync_frequency == 0: |
282 | | - with mx.stream(generation_stream): |
283 | | - loop_time = mx.distributed.all_sum(self._time_spent).item() |
| 281 | + loop_time = mx.distributed.all_sum(self._time_spent).item() |
284 | 282 | avg_loop_time = loop_time / ( |
285 | 283 | mx.distributed.init().size() * self._sync_frequency |
286 | 284 | ) |
@@ -308,94 +306,92 @@ def __init__(self, cli_args: argparse.Namespace): |
308 | 306 | ) |
309 | 307 | self.is_distributed = group.size() > 1 |
310 | 308 |
|
311 | | - # Preload the default model if it is provided |
312 | | - self.default_model_map = {} |
313 | | - if self.cli_args.model is not None: |
314 | | - self.default_model_map[self.cli_args.model] = "default_model" |
315 | | - self.load(self.cli_args.model, draft_model_path="default_model") |
| 309 | + # Maps model and adapter paths the actual paths to be used. Used to |
| 310 | + # map 'default_model' to the provided model by cli argument but could |
| 311 | + # be used for more in the future. |
| 312 | + self._model_map = {} |
| 313 | + self._adapter_map = {} |
| 314 | + self._draft_model_map = {} |
| 315 | + self._model_map["default_model"] = self.cli_args.model |
| 316 | + self._adapter_map["default_model"] = self.cli_args.adapter_path |
| 317 | + self._draft_model_map["default_model"] = self.cli_args.draft_model |
| 318 | + |
| 319 | + # Build the tokenizer config for later use in load |
| 320 | + self._tokenizer_config = { |
| 321 | + "trust_remote_code": True if cli_args.trust_remote_code else None |
| 322 | + } |
| 323 | + if cli_args.chat_template: |
| 324 | + self._tokenizer_config["chat_template"] = cli_args.chat_template |
316 | 325 |
|
317 | | - # Added in adapter_path to load dynamically |
318 | | - def load(self, model_path, adapter_path=None, draft_model_path=None): |
319 | | - model_path = self.default_model_map.get(model_path, model_path) |
320 | | - if self.model_key == (model_path, adapter_path, draft_model_path): |
321 | | - return self.model, self.tokenizer |
| 326 | + def _load(self, model_path, adapter_path=None, draft_model_path=None): |
| 327 | + if self.is_distributed and ( |
| 328 | + adapter_path is not None or draft_model_path is not None |
| 329 | + ): |
| 330 | + raise ValueError( |
| 331 | + "Loading with adapters or draft models not supported in distributed mode" |
| 332 | + ) |
322 | 333 |
|
323 | 334 | # Remove the old model if it exists. |
| 335 | + self.model_key = None |
324 | 336 | self.model = None |
325 | 337 | self.tokenizer = None |
326 | | - self.model_key = None |
327 | 338 | self.draft_model = None |
328 | 339 |
|
329 | | - # Building tokenizer_config |
330 | | - tokenizer_config = { |
331 | | - "trust_remote_code": True if self.cli_args.trust_remote_code else None |
332 | | - } |
333 | | - if self.cli_args.chat_template: |
334 | | - tokenizer_config["chat_template"] = self.cli_args.chat_template |
335 | | - |
336 | | - if model_path == "default_model": |
337 | | - if self.cli_args.model is None: |
338 | | - raise ValueError( |
339 | | - "A model path has to be given as a CLI " |
340 | | - "argument or in the HTTP request" |
341 | | - ) |
342 | | - adapter_path = adapter_path or self.cli_args.adapter_path |
343 | | - # TODO: Generalize distributed load |
344 | | - if self.is_distributed: |
345 | | - model, tokenizer = sharded_load( |
346 | | - self.cli_args.model, self.pipeline_group, self.tensor_group |
347 | | - ) |
348 | | - else: |
349 | | - model, tokenizer = load( |
350 | | - self.cli_args.model, |
351 | | - adapter_path=adapter_path, |
352 | | - tokenizer_config=tokenizer_config, |
353 | | - ) |
| 340 | + # Load the model and tokenizer |
| 341 | + if self.is_distributed: |
| 342 | + model, tokenizer = sharded_load( |
| 343 | + model_path, |
| 344 | + pipeline_group=self.pipeline_group, |
| 345 | + tensor_group=self.tensor_group, |
| 346 | + tokenizer_config=self._tokenizer_config, |
| 347 | + ) |
354 | 348 | else: |
355 | | - # TODO: Generalize distributed load |
356 | | - if self.is_distributed: |
357 | | - model, tokenizer = sharded_load( |
358 | | - model_path, self.pipeline_group, self.tensor_group |
359 | | - ) |
360 | | - else: |
361 | | - model, tokenizer = load( |
362 | | - model_path, |
363 | | - adapter_path=adapter_path, |
364 | | - tokenizer_config=tokenizer_config, |
365 | | - ) |
| 349 | + model, tokenizer = load( |
| 350 | + model_path, |
| 351 | + adapter_path=adapter_path, |
| 352 | + tokenizer_config=self._tokenizer_config, |
| 353 | + ) |
366 | 354 |
|
| 355 | + # Use the default chat template if needed |
367 | 356 | if self.cli_args.use_default_chat_template: |
368 | 357 | if tokenizer.chat_template is None: |
369 | 358 | tokenizer.chat_template = tokenizer.default_chat_template |
370 | 359 |
|
371 | | - self.model_key = (model_path, adapter_path, draft_model_path) |
372 | | - self.model = model |
373 | | - self.tokenizer = tokenizer |
374 | | - |
375 | | - def validate_draft_tokenizer(draft_tokenizer): |
376 | | - # Check if tokenizers are compatible |
| 360 | + # Load the draft model for speculative decoding |
| 361 | + draft_model = None |
| 362 | + if draft_model_path is not None: |
| 363 | + draft_model, draft_tokenizer = load(draft_model_path) |
377 | 364 | if draft_tokenizer.vocab_size != tokenizer.vocab_size: |
378 | 365 | logging.warning( |
379 | 366 | "Draft model tokenizer does not match model tokenizer. " |
380 | 367 | "Speculative decoding may not work as expected." |
381 | 368 | ) |
382 | 369 |
|
383 | | - # Load draft model if specified |
384 | | - if ( |
385 | | - draft_model_path == "default_model" |
386 | | - and self.cli_args.draft_model is not None |
387 | | - ): |
388 | | - self.draft_model, draft_tokenizer = load(self.cli_args.draft_model) |
389 | | - validate_draft_tokenizer(draft_tokenizer) |
| 370 | + # Compute batchability |
| 371 | + is_batchable = draft_model is None |
| 372 | + is_batchable = is_batchable and all( |
| 373 | + hasattr(c, "merge") for c in make_prompt_cache(model) |
| 374 | + ) |
| 375 | + |
| 376 | + # Update the member variables |
| 377 | + self.model_key = (model_path, adapter_path, draft_model_path) |
| 378 | + self.model = model |
| 379 | + self.tokenizer = tokenizer |
| 380 | + self.draft_model = draft_model |
| 381 | + self.is_batchable = is_batchable |
390 | 382 |
|
391 | | - elif draft_model_path is not None and draft_model_path != "default_model": |
392 | | - self.draft_model, draft_tokenizer = load(draft_model_path) |
393 | | - validate_draft_tokenizer(draft_tokenizer) |
| 383 | + def load_default(self): |
| 384 | + if self._model_map["default_model"] is not None: |
| 385 | + self.load("default_model", None, "default_model") |
394 | 386 |
|
395 | | - if self.draft_model is None: |
396 | | - self.is_batchable = all( |
397 | | - hasattr(c, "merge") for c in make_prompt_cache(self.model) |
398 | | - ) |
| 387 | + def load(self, model_path, adapter_path=None, draft_model_path=None): |
| 388 | + model_path = self._model_map.get(model_path, model_path) |
| 389 | + adapter_path = self._adapter_map.get(model_path, adapter_path) |
| 390 | + draft_model_path = self._draft_model_map.get(draft_model_path, draft_model_path) |
| 391 | + |
| 392 | + model_key = (model_path, adapter_path, draft_model_path) |
| 393 | + if self.model_key != model_key: |
| 394 | + self._load(*model_key) |
399 | 395 |
|
400 | 396 | return self.model, self.tokenizer |
401 | 397 |
|
@@ -489,22 +485,21 @@ def _share_object(self, obj): |
489 | 485 | if not self._is_distributed: |
490 | 486 | return obj |
491 | 487 |
|
492 | | - with mx.stream(generation_stream): |
493 | | - if self._rank == 0: |
494 | | - if obj is None: |
495 | | - mx.eval(mx.distributed.all_sum(0)) |
496 | | - return None |
497 | | - data = mx.array(pickle.dumps(obj)) |
498 | | - mx.eval(mx.distributed.all_sum(data.size)) |
499 | | - mx.eval(mx.distributed.all_sum(data)) |
500 | | - return obj |
501 | | - else: |
502 | | - size = mx.distributed.all_sum(0).item() |
503 | | - if size == 0: |
504 | | - return None |
505 | | - data = mx.zeros(size, dtype=mx.uint8) |
506 | | - data = mx.distributed.all_sum(data) |
507 | | - return pickle.loads(data) |
| 488 | + if self._rank == 0: |
| 489 | + if obj is None: |
| 490 | + mx.eval(mx.distributed.all_sum(0)) |
| 491 | + return None |
| 492 | + data = mx.array(pickle.dumps(obj)) |
| 493 | + mx.eval(mx.distributed.all_sum(data.size)) |
| 494 | + mx.eval(mx.distributed.all_sum(data)) |
| 495 | + return obj |
| 496 | + else: |
| 497 | + size = mx.distributed.all_sum(0).item() |
| 498 | + if size == 0: |
| 499 | + return None |
| 500 | + data = mx.zeros(size, dtype=mx.uint8) |
| 501 | + data = mx.distributed.all_sum(data) |
| 502 | + return pickle.loads(data) |
508 | 503 |
|
509 | 504 | def _share_request(self, request): |
510 | 505 | if not self._is_distributed: |
@@ -691,6 +686,14 @@ def _is_batchable(self, args): |
691 | 686 | return self.model_provider.is_batchable and args.seed is None |
692 | 687 |
|
693 | 688 | def _generate(self): |
| 689 | + # Local thread stream that we 'll pass to the BatchGenerator to make |
| 690 | + # sure that all generation runs in the same stream as the |
| 691 | + # synchronization messages. |
| 692 | + generation_stream = mx.default_stream(mx.default_device()) |
| 693 | + |
| 694 | + # Load the default model if it is given |
| 695 | + self.model_provider.load_default() |
| 696 | + |
694 | 697 | current_model = None |
695 | 698 | current_sampling = None |
696 | 699 | current_tokenizer = None |
@@ -820,6 +823,7 @@ def get_next_request(timeout=None): |
820 | 823 | completion_batch_size=self.cli_args.decode_concurrency, |
821 | 824 | prefill_batch_size=self.cli_args.prompt_concurrency, |
822 | 825 | prefill_step_size=self.cli_args.prefill_step_size, |
| 826 | + stream=generation_stream, |
823 | 827 | ) |
824 | 828 | unprocessed_requests.append((rqueue, request, args)) |
825 | 829 | continue |
@@ -909,12 +913,11 @@ def get_next_request(timeout=None): |
909 | 913 |
|
910 | 914 | uids_to_remove = self._share_object(uids_to_remove) |
911 | 915 | if uids_to_remove: |
912 | | - with mx.stream(generation_stream): |
913 | | - batch_generator.remove(uids_to_remove) |
914 | | - for uid in uids_to_remove: |
915 | | - # It may have already been removed during |
916 | | - # generation |
917 | | - batch_results.pop(uid, None) |
| 916 | + batch_generator.remove(uids_to_remove) |
| 917 | + for uid in uids_to_remove: |
| 918 | + # It may have already been removed during |
| 919 | + # generation |
| 920 | + batch_results.pop(uid, None) |
918 | 921 |
|
919 | 922 | def _serve_single(self, request): |
920 | 923 | rqueue, request, args = request |
|
0 commit comments