|
28 | 28 | import jax.numpy as jnp |
29 | 29 | from orbax import checkpoint as ocp |
30 | 30 | import qwix |
| 31 | +from qwix._src.core.qarray import QArray |
| 32 | +from qwix._src.providers.ptq import WithAux |
31 | 33 |
|
32 | 34 | from maxtext.common import checkpointing |
33 | 35 | from maxtext.configs import pyconfig |
@@ -607,37 +609,36 @@ def apply_lora_to_model( |
607 | 609 | ) |
608 | 610 |
|
609 | 611 | if mesh is not None: |
610 | | - with jax.set_mesh(mesh), nn_partitioning.axis_rules(mt_config.logical_axis_rules): |
611 | | - graph_def, state = nnx.split(lora_model) |
612 | | - |
613 | | - # We handle explicit replication for LoRA to ensure safety and efficiency. |
614 | | - state = jax.tree_util.tree_map( |
615 | | - lambda x: x.replace(sharding=jax.sharding.PartitionSpec(), out_sharding=None, sharding_names=None) |
616 | | - if isinstance(x, nnx.LoRAParam) |
617 | | - else x, |
618 | | - state, |
619 | | - is_leaf=lambda x: isinstance(x, nnx.Variable), |
620 | | - ) |
| 612 | + graph_def, state = nnx.split(lora_model) |
| 613 | + |
| 614 | + # We handle explicit replication for LoRA to ensure safety and efficiency. |
| 615 | + state = jax.tree_util.tree_map( |
| 616 | + lambda x: x.replace(sharding=jax.sharding.PartitionSpec(), out_sharding=None, sharding_names=None) |
| 617 | + if isinstance(x, nnx.LoRAParam) |
| 618 | + else x, |
| 619 | + state, |
| 620 | + is_leaf=lambda x: isinstance(x, nnx.Variable), |
| 621 | + ) |
621 | 622 |
|
622 | | - # Use logical_to_mesh_sharding to correctly map logical axes like 'embed' |
623 | | - # to physical mesh axes. |
624 | | - dst_shardings = nn.logical_to_mesh_sharding(nnx.get_partition_spec(state), mesh, mt_config.logical_axis_rules) |
| 623 | + # Use logical_to_mesh_sharding to correctly map logical axes like 'embed' |
| 624 | + # to physical mesh axes. |
| 625 | + dst_shardings = nn.logical_to_mesh_sharding(nnx.get_partition_spec(state), mesh, mt_config.logical_axis_rules) |
625 | 626 |
|
626 | | - def _safe_reshard(var, sharding_spec): |
627 | | - if not isinstance(var, nnx.Variable) or not isinstance(sharding_spec, jax.sharding.Sharding): |
628 | | - return var |
629 | | - val = var.get_value() |
630 | | - if not isinstance(val, jax.Array): |
631 | | - return var |
632 | | - # make_array_from_callback natively constructs a globally sharded array |
633 | | - # from the local host arrays, bypassing backend-specific device_put issues |
634 | | - # on both Pathways and McJAX. |
635 | | - resharded_val = jax.make_array_from_callback(val.shape, sharding_spec, lambda idx: val[idx]) |
636 | | - return var.replace(value=resharded_val) |
| 627 | + def _safe_reshard(var, sharding_spec): |
| 628 | + if not isinstance(var, nnx.Variable) or not isinstance(sharding_spec, jax.sharding.Sharding): |
| 629 | + return var |
| 630 | + val = var.get_value() |
| 631 | + if not isinstance(val, jax.Array): |
| 632 | + return var |
| 633 | + # make_array_from_callback natively constructs a globally sharded array |
| 634 | + # from the local host arrays, bypassing backend-specific device_put issues |
| 635 | + # on both Pathways and McJAX. |
| 636 | + resharded_val = jax.make_array_from_callback(val.shape, sharding_spec, lambda idx: val[idx]) |
| 637 | + return var.replace(value=resharded_val) |
637 | 638 |
|
638 | | - state = jax.tree_util.tree_map(_safe_reshard, state, dst_shardings, is_leaf=lambda x: isinstance(x, nnx.Variable)) |
| 639 | + state = jax.tree_util.tree_map(_safe_reshard, state, dst_shardings, is_leaf=lambda x: isinstance(x, nnx.Variable)) |
639 | 640 |
|
640 | | - lora_model = nnx.merge(graph_def, state) |
| 641 | + lora_model = nnx.merge(graph_def, state) |
641 | 642 |
|
642 | 643 | _verify_lora_parameters(lora_model, mt_config) |
643 | 644 |
|
@@ -699,37 +700,54 @@ def restore_lora_from_path(model: nnx.Module, mt_config: pyconfig.HyperParameter |
699 | 700 | max_logging.log(f"Guided restore failed: {e}. Falling back to basic restore.") |
700 | 701 | restored_lora_params = ocp.PyTreeCheckpointer().restore(lora_restore_path) |
701 | 702 |
|
| 703 | + # If restoring from a full TrainState checkpoint, navigate into the model sub-tree |
| 704 | + if isinstance(restored_lora_params, dict) and "model" in restored_lora_params: |
| 705 | + restored_lora_params = restored_lora_params["model"] |
| 706 | + elif hasattr(restored_lora_params, "model"): |
| 707 | + restored_lora_params = getattr(restored_lora_params, "model") |
| 708 | + restored_count = 0 |
| 709 | + |
702 | 710 | # Post processing |
703 | 711 | def _map_to_state(path, variable): |
| 712 | + nonlocal restored_count |
704 | 713 | if not isinstance(variable, nnx.Variable): |
705 | 714 | return |
706 | 715 |
|
707 | 716 | str_path = [str(k.key if hasattr(k, "key") else (k.name if hasattr(k, "name") else k)) for k in path] |
708 | 717 |
|
709 | 718 | curr = restored_lora_params |
710 | 719 | for p in str_path: |
711 | | - if isinstance(curr, dict) and p in curr: |
712 | | - curr = curr[p] |
| 720 | + if isinstance(curr, Mapping): |
| 721 | + if p in curr: |
| 722 | + curr = curr[p] |
| 723 | + elif p.isdigit() and int(p) in curr: |
| 724 | + curr = curr[int(p)] |
| 725 | + else: |
| 726 | + return |
713 | 727 | elif hasattr(curr, p): |
714 | 728 | curr = getattr(curr, p) |
715 | 729 | else: |
716 | 730 | return |
717 | 731 |
|
718 | | - if isinstance(curr, dict) and "value" in curr: |
| 732 | + if isinstance(curr, Mapping) and "value" in curr: |
719 | 733 | matched_val = curr["value"] |
720 | 734 | elif hasattr(curr, "value"): |
721 | 735 | matched_val = getattr(curr, "value") |
722 | 736 | else: |
723 | 737 | matched_val = curr |
724 | 738 |
|
725 | 739 | variable.value = matched_val |
| 740 | + restored_count += 1 |
726 | 741 |
|
727 | 742 | jax.tree_util.tree_map_with_path( |
728 | 743 | _map_to_state, |
729 | 744 | abstract_lora_params, |
730 | 745 | is_leaf=lambda n: isinstance(n, nnx.Variable), |
731 | 746 | ) |
732 | 747 |
|
| 748 | + if restored_count == 0: |
| 749 | + raise ValueError(f"No LoRA/adapter parameters were successfully restored from checkpoint at '{lora_restore_path}'.") |
| 750 | + |
733 | 751 | nnx.update(model, abstract_lora_params) |
734 | 752 | max_logging.log(f"LoRA restore complete from '{lora_restore_path}'.") |
735 | 753 | return model |
@@ -918,3 +936,26 @@ def add_lora(out_node, base_node, path): |
918 | 936 | opt_state={}, |
919 | 937 | ) |
920 | 938 | return unboxed_abstract_lora_state, lora_state_mesh_annotations |
| 939 | + |
| 940 | + |
| 941 | +def restore_qlora_base_weights(val): |
| 942 | + """Restores qwix custom quantized types from nnx.State representation.""" |
| 943 | + if isinstance(val, nnx.State): |
| 944 | + pure_dict = {k: restore_qlora_base_weights(v) for k, v in val.items()} |
| 945 | + if "array" in pure_dict: |
| 946 | + return WithAux(array=pure_dict["array"], how=None) |
| 947 | + elif "qvalue" in pure_dict and "scale" in pure_dict: |
| 948 | + return QArray( |
| 949 | + qvalue=pure_dict["qvalue"], |
| 950 | + scale=pure_dict["scale"], |
| 951 | + zero_point=pure_dict.get("zero_point", None), |
| 952 | + qtype=None, |
| 953 | + ) |
| 954 | + else: |
| 955 | + return pure_dict |
| 956 | + elif isinstance(val, nnx.Variable): |
| 957 | + return restore_qlora_base_weights(val.get_value()) |
| 958 | + elif isinstance(val, dict): |
| 959 | + return {k: restore_qlora_base_weights(v) for k, v in val.items()} |
| 960 | + else: |
| 961 | + return val |
0 commit comments