From 45572dedd86bde7ebed5e6b25307adf6de6b6d2c Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 16 Apr 2026 11:47:29 +0000 Subject: [PATCH 01/21] BUG: fix multi-GPU inference crash for bnb 4-bit/8-bit models When load_in_4bit=True is used with device_map="sequential" and the model is placed across multiple GPUs (or entirely on a non-default GPU like cuda:1), the bitsandbytes loading path in transformers places weights on the target CUDA devices but never calls dispatch_model. This leaves zero AlignDevicesHook instances installed, so the first cross-device forward call crashes with: RuntimeError: Expected all tensors to be on the same device, but got index is on cuda:0, different from other tensors on cuda:1 This commonly happens on Kaggle 2xT4 setups where Gemma-4-31B in 4-bit (~17 GB) does not fit on a single 15 GB T4, forcing accelerate to shard across both devices. Fix: after from_pretrained returns a bnb-quantized model, infer a device map from post-load parameter placement and call accelerate's attach_align_device_hook_on_blocks to install input-routing hooks. Two scenarios are handled: 1. Multi-device (weights span multiple GPUs): per-block hooks route each module's inputs to the device holding its weights. 2. Single non-default device (all weights on e.g. cuda:1 because cuda:0 was too small): a root-level hook with io_same_device=True auto-moves inputs from any device to the model's device. Guards ensure zero overhead for the common single-GPU-on-cuda:0 path: - Only activates for load_in_4bit or load_in_8bit - Skips fast_inference (vLLM), offload_embedding, already-dispatched models - Skips when all weights are already on cuda:0 Tested on 2x B200 with Gemma-4-31B-it-unsloth-bnb-4bit across 6 different max_memory configurations (1 GiB to 15 GiB per device). All pass. --- unsloth/models/vision.py | 186 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index e31617f89a..9ba0bb0e50 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -97,6 +97,180 @@ __all__ = [ "FastBaseModel", ] + +def _infer_device_map_from_loaded_model(model): + """ + Build a compact device_map dict by inspecting where each parameter of + *model* actually lives after a bitsandbytes multi-device load. + + The resulting map satisfies accelerate's check_device_map invariant: + every parameter in the state_dict is covered by exactly one prefix key. + + Algorithm: recurse over the module tree top-down. When all parameters in + a subtree share the same device, emit one entry for the whole subtree. + Otherwise recurse into children and emit entries for direct-parameter + leaves that are not already covered by a child entry. + """ + device_map = {} + + def _assign(module, prefix): + params = list(module.named_parameters(remove_duplicate=False)) + if not params: + bufs = list(module.named_buffers()) + if bufs: + device_map[prefix] = bufs[0][1].device + return + devices = {p.device for _, p in params} + if len(devices) == 1: + device_map[prefix] = next(iter(devices)) + else: + for child_name, child in module.named_children(): + child_prefix = f"{prefix}.{child_name}" if prefix else child_name + _assign(child, child_prefix) + for pname, param in module.named_parameters(remove_duplicate=False): + if "." not in pname: + full = f"{prefix}.{pname}" if prefix else pname + if not any( + full == k or full.startswith(k + ".") + for k in device_map + ): + device_map[full] = param.device + + _assign(model, "") + # Remove the root key only when child entries already cover all params. + # For single-device models, "" may be the sole entry and must be kept. + if "" in device_map and len(device_map) > 1: + device_map.pop("") + return device_map + + +def _attach_bnb_multidevice_hooks( + model, load_in_4bit, load_in_8bit, offload_embedding, fast_inference +): + """ + Retroactively attach accelerate AlignDevicesHook on a bitsandbytes- + quantised model that was loaded with a multi-device (or non-default-device) + device_map. + + When load_in_4bit or load_in_8bit is used together with an accelerate + device_map, AutoModel.from_pretrained places weights on the target CUDA + devices but does NOT call dispatch_model, so no AlignDevicesHook is + installed. Any cross-device forward pass then crashes with: + RuntimeError: Expected all tensors to be on the same device + + This function fixes that by installing hooks after loading, without + re-moving any quantised weight tensors. Two scenarios are handled: + + 1. Multi-device: weights span multiple CUDA devices. Per-block hooks + route each module's inputs to the correct device. + 2. Single non-default device: all weights land on e.g. cuda:1 but the + caller may pass inputs on cuda:0. A root-level hook fixes this. + + Guards + ------ + - Only runs when load_in_4bit or load_in_8bit is True. + - Skips the vLLM path (fast_inference=True). + - Skips the offload_embedding CPU-offload path (handled separately). + - Skips models that already have hf_device_map set (already dispatched). + - Skips models with no CUDA parameters (CPU-only or meta-device paths). + """ + if fast_inference: + return + if not (load_in_4bit or load_in_8bit): + return + if offload_embedding: + return + if getattr(model, "hf_device_map", None) is not None: + return # already dispatched + + try: + cuda_devs = { + p.device + for p in model.parameters() + if hasattr(p, "device") and p.device.type == "cuda" + } + except Exception: + return + + if not cuda_devs: + return # no CUDA parameters -- nothing to do + + # All weights on the default device -- the common single-GPU case. + default_cuda = torch.device("cuda", 0) + if cuda_devs == {default_cuda}: + return + + try: + from accelerate.hooks import attach_align_device_hook_on_blocks + from accelerate.utils import find_tied_parameters, retie_parameters + except ImportError: + return # accelerate not available + + try: + inferred_map = _infer_device_map_from_loaded_model(model) + if not inferred_map: + return + + # Determine the "main" device (first CUDA device encountered). + cuda_device_vals = [ + v for v in inferred_map.values() + if isinstance(v, torch.device) and v.type == "cuda" + ] + main_device = cuda_device_vals[0] if cuda_device_vals else next(iter(cuda_devs)) + + # Preserve tied-parameter references before installing hooks. + tied_params = find_tied_parameters(model) + + if len(cuda_devs) > 1: + # Multi-device: each block's hook routes inputs to its own device. + execution_device = dict(inferred_map) + execution_device[""] = main_device + offload_dict = {k: False for k in execution_device} + attach_align_device_hook_on_blocks( + model, + execution_device=execution_device, + offload=offload_dict, + weights_map=None, + offload_buffers=False, + ) + desc = f"{len(inferred_map)} block(s) across {len(cuda_devs)} device(s)" + else: + # Single non-default device: one root hook sends all inputs to it. + attach_align_device_hook_on_blocks( + model, + execution_device=main_device, + offload=False, + weights_map=None, + offload_buffers=False, + ) + desc = f"root hook -> {main_device} (single non-default device)" + + # Retie parameters that hook installation may have unlinked. + retie_parameters(model, tied_params) + + # Expose hf_device_map so downstream generation helpers work. + # Use the device index (int) as the value, matching HF's convention. + model.hf_device_map = { + k: v.index if isinstance(v, torch.device) else v + for k, v in inferred_map.items() + } + + print( + f"Unsloth: Attached accelerate AlignDevicesHook ({desc}) " + f"for bnb multi-GPU inference." + ) + except Exception as exc: + import warnings + warnings.warn( + f"Unsloth: Could not attach multi-device dispatch hooks automatically " + f"({type(exc).__name__}: {exc}). " + "Cross-device inference may fail. Consider using a single GPU or " + "calling accelerate.dispatch_model() manually.", + RuntimeWarning, + stacklevel=2, + ) + + global NUM_LOGITS_TO_KEEP NUM_LOGITS_TO_KEEP = dict() @@ -835,6 +1009,18 @@ class FastBaseModel: # attn_implementation = attn_implementation, **kwargs, ) + # Attach AlignDevicesHook for bnb multi-device / non-default-device + # loads. The bnb loading path places weights on CUDA devices but + # never calls dispatch_model, so no hooks are installed. Without + # hooks, any cross-device module call crashes at forward time with + # "Expected all tensors to be on the same device". + _attach_bnb_multidevice_hooks( + model, + load_in_4bit = load_in_4bit, + load_in_8bit = load_in_8bit, + offload_embedding = offload_embedding, + fast_inference = fast_inference, + ) if hasattr(model, "generate"): model.fast_generate = make_fast_generate_wrapper(model.generate) model.fast_generate_batches = error_out_no_vllm From bed825e04a0ae907267a6c868f35efb6d1c9f8d3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:48:56 +0000 Subject: [PATCH 02/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/models/vision.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index 9ba0bb0e50..c0dcb4c971 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -114,7 +114,7 @@ def _infer_device_map_from_loaded_model(model): device_map = {} def _assign(module, prefix): - params = list(module.named_parameters(remove_duplicate=False)) + params = list(module.named_parameters(remove_duplicate = False)) if not params: bufs = list(module.named_buffers()) if bufs: @@ -127,12 +127,11 @@ def _infer_device_map_from_loaded_model(model): for child_name, child in module.named_children(): child_prefix = f"{prefix}.{child_name}" if prefix else child_name _assign(child, child_prefix) - for pname, param in module.named_parameters(remove_duplicate=False): + for pname, param in module.named_parameters(remove_duplicate = False): if "." not in pname: full = f"{prefix}.{pname}" if prefix else pname if not any( - full == k or full.startswith(k + ".") - for k in device_map + full == k or full.startswith(k + ".") for k in device_map ): device_map[full] = param.device @@ -213,7 +212,8 @@ def _attach_bnb_multidevice_hooks( # Determine the "main" device (first CUDA device encountered). cuda_device_vals = [ - v for v in inferred_map.values() + v + for v in inferred_map.values() if isinstance(v, torch.device) and v.type == "cuda" ] main_device = cuda_device_vals[0] if cuda_device_vals else next(iter(cuda_devs)) @@ -228,20 +228,20 @@ def _attach_bnb_multidevice_hooks( offload_dict = {k: False for k in execution_device} attach_align_device_hook_on_blocks( model, - execution_device=execution_device, - offload=offload_dict, - weights_map=None, - offload_buffers=False, + execution_device = execution_device, + offload = offload_dict, + weights_map = None, + offload_buffers = False, ) desc = f"{len(inferred_map)} block(s) across {len(cuda_devs)} device(s)" else: # Single non-default device: one root hook sends all inputs to it. attach_align_device_hook_on_blocks( model, - execution_device=main_device, - offload=False, - weights_map=None, - offload_buffers=False, + execution_device = main_device, + offload = False, + weights_map = None, + offload_buffers = False, ) desc = f"root hook -> {main_device} (single non-default device)" @@ -261,13 +261,14 @@ def _attach_bnb_multidevice_hooks( ) except Exception as exc: import warnings + warnings.warn( f"Unsloth: Could not attach multi-device dispatch hooks automatically " f"({type(exc).__name__}: {exc}). " "Cross-device inference may fail. Consider using a single GPU or " "calling accelerate.dispatch_model() manually.", RuntimeWarning, - stacklevel=2, + stacklevel = 2, ) @@ -1016,10 +1017,10 @@ class FastBaseModel: # "Expected all tensors to be on the same device". _attach_bnb_multidevice_hooks( model, - load_in_4bit = load_in_4bit, - load_in_8bit = load_in_8bit, + load_in_4bit = load_in_4bit, + load_in_8bit = load_in_8bit, offload_embedding = offload_embedding, - fast_inference = fast_inference, + fast_inference = fast_inference, ) if hasattr(model, "generate"): model.fast_generate = make_fast_generate_wrapper(model.generate) From 8603233829ccb64bc7061cf993c9b601f19554fb Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 16 Apr 2026 12:47:02 +0000 Subject: [PATCH 03/21] fix: use dispatch_model and strip _is_hf_initialized for bnb multi-GPU hooks Two bugs found during testing: 1. accelerate's set_module_tensor_to_device passes param.__dict__ as kwargs to Params4bit(), but HF Transformers adds _is_hf_initialized to that dict. Params4bit.__new__() does not accept it, causing TypeError. Fix: strip the key before dispatching, restore after. 2. attach_align_device_hook_on_blocks with a block-level device map only installs coarse-grained hooks. Sub-modules (e.g. RMSNorm) inherit the wrong execution device, causing cross-device errors at forward time. Fix: use dispatch_model which installs hooks at every sub-module level, matching HF's own from_pretrained behavior. Tested with 28 tests across unit, single-GPU, multi-GPU (gemma-4-31B 4bit on 2x B200), and edge case suites -- all passing. --- unsloth/models/vision.py | 67 +++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index c0dcb4c971..b4d5ccff23 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -200,7 +200,7 @@ def _attach_bnb_multidevice_hooks( return try: - from accelerate.hooks import attach_align_device_hook_on_blocks + from accelerate import dispatch_model from accelerate.utils import find_tied_parameters, retie_parameters except ImportError: return # accelerate not available @@ -210,51 +210,40 @@ def _attach_bnb_multidevice_hooks( if not inferred_map: return - # Determine the "main" device (first CUDA device encountered). - cuda_device_vals = [ - v - for v in inferred_map.values() - if isinstance(v, torch.device) and v.type == "cuda" - ] - main_device = cuda_device_vals[0] if cuda_device_vals else next(iter(cuda_devs)) - - # Preserve tied-parameter references before installing hooks. + # Preserve tied-parameter references before dispatching. tied_params = find_tied_parameters(model) - if len(cuda_devs) > 1: - # Multi-device: each block's hook routes inputs to its own device. - execution_device = dict(inferred_map) - execution_device[""] = main_device - offload_dict = {k: False for k in execution_device} - attach_align_device_hook_on_blocks( - model, - execution_device = execution_device, - offload = offload_dict, - weights_map = None, - offload_buffers = False, - ) + # accelerate's set_module_tensor_to_device grabs param.__dict__ and + # passes it as **kwargs to the parameter class constructor. HF + # Transformers adds _is_hf_initialized to parameter __dict__ which + # bitsandbytes Params4bit/Int8Params do not accept, causing TypeError. + # Strip these extra keys before dispatching. + _extra_keys = ("_is_hf_initialized",) + _stripped = [] + for _, param in model.named_parameters(): + for key in _extra_keys: + if key in param.__dict__: + _stripped.append((param, key, param.__dict__.pop(key))) + + try: + # Convert device_map values from torch.device to int (HF convention) + device_map_int = { + k: v.index if isinstance(v, torch.device) and v.type == "cuda" else v + for k, v in inferred_map.items() + } + + # dispatch_model installs AlignDevicesHook on every module and + # sub-module, exactly matching HF's own loading behavior. + dispatch_model(model, device_map = device_map_int) desc = f"{len(inferred_map)} block(s) across {len(cuda_devs)} device(s)" - else: - # Single non-default device: one root hook sends all inputs to it. - attach_align_device_hook_on_blocks( - model, - execution_device = main_device, - offload = False, - weights_map = None, - offload_buffers = False, - ) - desc = f"root hook -> {main_device} (single non-default device)" + finally: + # Restore stripped keys + for param, key, val in _stripped: + param.__dict__[key] = val # Retie parameters that hook installation may have unlinked. retie_parameters(model, tied_params) - # Expose hf_device_map so downstream generation helpers work. - # Use the device index (int) as the value, matching HF's convention. - model.hf_device_map = { - k: v.index if isinstance(v, torch.device) else v - for k, v in inferred_map.items() - } - print( f"Unsloth: Attached accelerate AlignDevicesHook ({desc}) " f"for bnb multi-GPU inference." From ab18542b7b90348423ea4df7fb7f884b4508dcc1 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 16 Apr 2026 14:20:50 +0000 Subject: [PATCH 04/21] fix: handle single-device non-default GPU and warn on parameter errors dispatch_model skips AlignDevicesHook installation for single-device maps (accelerate fast-path). When all weights sit on a non-default GPU (e.g. cuda:1) the caller's inputs may still arrive on cuda:0, causing a device mismatch. Detect this and manually add a root-level AlignDevicesHook to route inputs. Also replace the silent except-and-return on CUDA device collection with a RuntimeWarning so users can debug failures. --- unsloth/models/vision.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index b4d5ccff23..57b0d02c3e 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -188,7 +188,14 @@ def _attach_bnb_multidevice_hooks( for p in model.parameters() if hasattr(p, "device") and p.device.type == "cuda" } - except Exception: + except Exception as exc: + import warnings + warnings.warn( + "Unsloth: Failed to determine CUDA devices from model parameters, " + f"so multi-GPU hooks cannot be attached. ({type(exc).__name__}: {exc})", + RuntimeWarning, + stacklevel = 2, + ) return if not cuda_devs: @@ -201,6 +208,7 @@ def _attach_bnb_multidevice_hooks( try: from accelerate import dispatch_model + from accelerate.hooks import AlignDevicesHook, add_hook_to_module from accelerate.utils import find_tied_parameters, retie_parameters except ImportError: return # accelerate not available @@ -235,6 +243,16 @@ def _attach_bnb_multidevice_hooks( # dispatch_model installs AlignDevicesHook on every module and # sub-module, exactly matching HF's own loading behavior. dispatch_model(model, device_map = device_map_int) + + # dispatch_model skips hook installation for single-device maps + # (accelerate fast-path). When all weights sit on a non-default + # GPU (e.g. cuda:1) the caller's inputs may still arrive on + # cuda:0, causing a device mismatch. Detect this and manually + # add a root-level AlignDevicesHook to route inputs. + if len(cuda_devs) == 1 and not hasattr(model, "_hf_hook"): + main_device = next(iter(cuda_devs)) + add_hook_to_module(model, AlignDevicesHook(execution_device = main_device)) + desc = f"{len(inferred_map)} block(s) across {len(cuda_devs)} device(s)" finally: # Restore stripped keys From 42b29a35acaef978b6b73db8ad456ca1a2a99140 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:21:06 +0000 Subject: [PATCH 05/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/models/vision.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index 57b0d02c3e..284b29f45b 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -190,6 +190,7 @@ def _attach_bnb_multidevice_hooks( } except Exception as exc: import warnings + warnings.warn( "Unsloth: Failed to determine CUDA devices from model parameters, " f"so multi-GPU hooks cannot be attached. ({type(exc).__name__}: {exc})", @@ -251,7 +252,9 @@ def _attach_bnb_multidevice_hooks( # add a root-level AlignDevicesHook to route inputs. if len(cuda_devs) == 1 and not hasattr(model, "_hf_hook"): main_device = next(iter(cuda_devs)) - add_hook_to_module(model, AlignDevicesHook(execution_device = main_device)) + add_hook_to_module( + model, AlignDevicesHook(execution_device = main_device) + ) desc = f"{len(inferred_map)} block(s) across {len(cuda_devs)} device(s)" finally: From d6b758035af2e527232776f1140b048bfa873fd4 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 16 Apr 2026 14:41:44 +0000 Subject: [PATCH 06/21] fix: address review findings -- force_hooks, CPU device conversion, llama.py path Three fixes from 13-reviewer consensus: 1. Use dispatch_model(force_hooks=True) instead of manual AlignDevicesHook fallback. This properly handles single-device non-default GPU maps (e.g. all weights on cuda:1) with io_same_device routing, and eliminates the need for the separate add_hook_to_module call. 2. Convert non-CUDA devices (cpu, meta) to type strings in device_map_int. dispatch_model uses string equality (device == "cpu") internally, so torch.device("cpu") must become "cpu" not stay as an object. 3. Add _attach_bnb_multidevice_hooks call to FastLlamaModel.from_pretrained in llama.py. The default 4-bit FastLanguageModel path routes through llama.py:2482, not FastBaseModel, so the hook was never called for the most common use case (Llama, Gemma, Mistral, etc.). Also removes redundant retie_parameters call (dispatch_model already calls it internally) and unused imports. --- unsloth/models/llama.py | 11 +++++++++++ unsloth/models/vision.py | 33 ++++++++++----------------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/unsloth/models/llama.py b/unsloth/models/llama.py index 425df1c084..98f25c7dc7 100644 --- a/unsloth/models/llama.py +++ b/unsloth/models/llama.py @@ -2490,6 +2490,17 @@ class FastLlamaModel: attn_implementation = preferred_attn_impl, **kwargs, ) + # Attach AlignDevicesHook for bnb multi-device / non-default-device + # loads. The bnb loading path places weights on CUDA devices but + # never calls dispatch_model, so no hooks are installed. + from unsloth.models.vision import _attach_bnb_multidevice_hooks + _attach_bnb_multidevice_hooks( + model, + load_in_4bit = load_in_4bit, + load_in_8bit = False, + offload_embedding = False, + fast_inference = False, + ) model.fast_generate = make_fast_generate_wrapper(model.generate) model.fast_generate_batches = None else: diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index 284b29f45b..76a74631b1 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -209,8 +209,6 @@ def _attach_bnb_multidevice_hooks( try: from accelerate import dispatch_model - from accelerate.hooks import AlignDevicesHook, add_hook_to_module - from accelerate.utils import find_tied_parameters, retie_parameters except ImportError: return # accelerate not available @@ -219,9 +217,6 @@ def _attach_bnb_multidevice_hooks( if not inferred_map: return - # Preserve tied-parameter references before dispatching. - tied_params = find_tied_parameters(model) - # accelerate's set_module_tensor_to_device grabs param.__dict__ and # passes it as **kwargs to the parameter class constructor. HF # Transformers adds _is_hf_initialized to parameter __dict__ which @@ -235,36 +230,28 @@ def _attach_bnb_multidevice_hooks( _stripped.append((param, key, param.__dict__.pop(key))) try: - # Convert device_map values from torch.device to int (HF convention) + # Convert device_map values to the format dispatch_model expects: + # CUDA devices -> int index, non-CUDA devices -> type string. + # dispatch_model uses string equality (device == "cpu") internally, + # so torch.device("cpu") must become "cpu", not stay as an object. device_map_int = { - k: v.index if isinstance(v, torch.device) and v.type == "cuda" else v + k: (v.index if v.type == "cuda" else v.type) + if isinstance(v, torch.device) else v for k, v in inferred_map.items() } # dispatch_model installs AlignDevicesHook on every module and # sub-module, exactly matching HF's own loading behavior. - dispatch_model(model, device_map = device_map_int) - - # dispatch_model skips hook installation for single-device maps - # (accelerate fast-path). When all weights sit on a non-default - # GPU (e.g. cuda:1) the caller's inputs may still arrive on - # cuda:0, causing a device mismatch. Detect this and manually - # add a root-level AlignDevicesHook to route inputs. - if len(cuda_devs) == 1 and not hasattr(model, "_hf_hook"): - main_device = next(iter(cuda_devs)) - add_hook_to_module( - model, AlignDevicesHook(execution_device = main_device) - ) - + # force_hooks=True ensures hooks are installed even for single- + # device maps (e.g. all weights on cuda:1), where the default + # fast-path would skip hook installation entirely. + dispatch_model(model, device_map = device_map_int, force_hooks = True) desc = f"{len(inferred_map)} block(s) across {len(cuda_devs)} device(s)" finally: # Restore stripped keys for param, key, val in _stripped: param.__dict__[key] = val - # Retie parameters that hook installation may have unlinked. - retie_parameters(model, tied_params) - print( f"Unsloth: Attached accelerate AlignDevicesHook ({desc}) " f"for bnb multi-GPU inference." From bf3171e567172d7dad6a086dfa52845d01aa920f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:43:14 +0000 Subject: [PATCH 07/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/models/llama.py | 7 ++++--- unsloth/models/vision.py | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/unsloth/models/llama.py b/unsloth/models/llama.py index 98f25c7dc7..0913cf9de2 100644 --- a/unsloth/models/llama.py +++ b/unsloth/models/llama.py @@ -2494,12 +2494,13 @@ class FastLlamaModel: # loads. The bnb loading path places weights on CUDA devices but # never calls dispatch_model, so no hooks are installed. from unsloth.models.vision import _attach_bnb_multidevice_hooks + _attach_bnb_multidevice_hooks( model, - load_in_4bit = load_in_4bit, - load_in_8bit = False, + load_in_4bit = load_in_4bit, + load_in_8bit = False, offload_embedding = False, - fast_inference = False, + fast_inference = False, ) model.fast_generate = make_fast_generate_wrapper(model.generate) model.fast_generate_batches = None diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index 76a74631b1..376e00a1fa 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -236,7 +236,8 @@ def _attach_bnb_multidevice_hooks( # so torch.device("cpu") must become "cpu", not stay as an object. device_map_int = { k: (v.index if v.type == "cuda" else v.type) - if isinstance(v, torch.device) else v + if isinstance(v, torch.device) + else v for k, v in inferred_map.items() } From 0df61f13e4c29b96733d400cebde21739bacf68a Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 16 Apr 2026 14:54:45 +0000 Subject: [PATCH 08/21] style: shorten inline comments --- unsloth/models/llama.py | 4 +--- unsloth/models/vision.py | 20 +++++--------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/unsloth/models/llama.py b/unsloth/models/llama.py index 0913cf9de2..d145f08bd5 100644 --- a/unsloth/models/llama.py +++ b/unsloth/models/llama.py @@ -2490,9 +2490,7 @@ class FastLlamaModel: attn_implementation = preferred_attn_impl, **kwargs, ) - # Attach AlignDevicesHook for bnb multi-device / non-default-device - # loads. The bnb loading path places weights on CUDA devices but - # never calls dispatch_model, so no hooks are installed. + # Attach dispatch hooks for bnb multi-device loads. from unsloth.models.vision import _attach_bnb_multidevice_hooks _attach_bnb_multidevice_hooks( diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index 376e00a1fa..f649e5fa40 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -217,11 +217,8 @@ def _attach_bnb_multidevice_hooks( if not inferred_map: return - # accelerate's set_module_tensor_to_device grabs param.__dict__ and - # passes it as **kwargs to the parameter class constructor. HF - # Transformers adds _is_hf_initialized to parameter __dict__ which - # bitsandbytes Params4bit/Int8Params do not accept, causing TypeError. - # Strip these extra keys before dispatching. + # Strip _is_hf_initialized from param.__dict__ -- bnb constructors + # reject it when accelerate re-creates params during dispatch. _extra_keys = ("_is_hf_initialized",) _stripped = [] for _, param in model.named_parameters(): @@ -230,10 +227,7 @@ def _attach_bnb_multidevice_hooks( _stripped.append((param, key, param.__dict__.pop(key))) try: - # Convert device_map values to the format dispatch_model expects: - # CUDA devices -> int index, non-CUDA devices -> type string. - # dispatch_model uses string equality (device == "cpu") internally, - # so torch.device("cpu") must become "cpu", not stay as an object. + # CUDA -> int index, non-CUDA -> type string ("cpu", "meta"). device_map_int = { k: (v.index if v.type == "cuda" else v.type) if isinstance(v, torch.device) @@ -241,15 +235,11 @@ def _attach_bnb_multidevice_hooks( for k, v in inferred_map.items() } - # dispatch_model installs AlignDevicesHook on every module and - # sub-module, exactly matching HF's own loading behavior. - # force_hooks=True ensures hooks are installed even for single- - # device maps (e.g. all weights on cuda:1), where the default - # fast-path would skip hook installation entirely. + # force_hooks=True: install hooks even for single-device maps. dispatch_model(model, device_map = device_map_int, force_hooks = True) desc = f"{len(inferred_map)} block(s) across {len(cuda_devs)} device(s)" finally: - # Restore stripped keys + # Restore stripped keys. for param, key, val in _stripped: param.__dict__[key] = val From 01c5c1a58165490ced74bbfee87b1dc76b655783 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 16 Apr 2026 15:06:46 +0000 Subject: [PATCH 09/21] style: trim docstrings and comments --- unsloth/models/vision.py | 49 +++++----------------------------------- 1 file changed, 6 insertions(+), 43 deletions(-) diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index f649e5fa40..85aa4b62b9 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -99,18 +99,7 @@ __all__ = [ def _infer_device_map_from_loaded_model(model): - """ - Build a compact device_map dict by inspecting where each parameter of - *model* actually lives after a bitsandbytes multi-device load. - - The resulting map satisfies accelerate's check_device_map invariant: - every parameter in the state_dict is covered by exactly one prefix key. - - Algorithm: recurse over the module tree top-down. When all parameters in - a subtree share the same device, emit one entry for the whole subtree. - Otherwise recurse into children and emit entries for direct-parameter - leaves that are not already covered by a child entry. - """ + """Build a compact device_map by inspecting actual parameter placements.""" device_map = {} def _assign(module, prefix): @@ -136,8 +125,6 @@ def _infer_device_map_from_loaded_model(model): device_map[full] = param.device _assign(model, "") - # Remove the root key only when child entries already cover all params. - # For single-device models, "" may be the sole entry and must be kept. if "" in device_map and len(device_map) > 1: device_map.pop("") return device_map @@ -147,31 +134,9 @@ def _attach_bnb_multidevice_hooks( model, load_in_4bit, load_in_8bit, offload_embedding, fast_inference ): """ - Retroactively attach accelerate AlignDevicesHook on a bitsandbytes- - quantised model that was loaded with a multi-device (or non-default-device) - device_map. - - When load_in_4bit or load_in_8bit is used together with an accelerate - device_map, AutoModel.from_pretrained places weights on the target CUDA - devices but does NOT call dispatch_model, so no AlignDevicesHook is - installed. Any cross-device forward pass then crashes with: - RuntimeError: Expected all tensors to be on the same device - - This function fixes that by installing hooks after loading, without - re-moving any quantised weight tensors. Two scenarios are handled: - - 1. Multi-device: weights span multiple CUDA devices. Per-block hooks - route each module's inputs to the correct device. - 2. Single non-default device: all weights land on e.g. cuda:1 but the - caller may pass inputs on cuda:0. A root-level hook fixes this. - - Guards - ------ - - Only runs when load_in_4bit or load_in_8bit is True. - - Skips the vLLM path (fast_inference=True). - - Skips the offload_embedding CPU-offload path (handled separately). - - Skips models that already have hf_device_map set (already dispatched). - - Skips models with no CUDA parameters (CPU-only or meta-device paths). + Attach accelerate AlignDevicesHook on a bnb model loaded across multiple + devices (or a non-default device). No-op for single-GPU cuda:0, non-bnb, + vLLM, or already-dispatched models. """ if fast_inference: return @@ -200,9 +165,8 @@ def _attach_bnb_multidevice_hooks( return if not cuda_devs: - return # no CUDA parameters -- nothing to do + return - # All weights on the default device -- the common single-GPU case. default_cuda = torch.device("cuda", 0) if cuda_devs == {default_cuda}: return @@ -217,8 +181,7 @@ def _attach_bnb_multidevice_hooks( if not inferred_map: return - # Strip _is_hf_initialized from param.__dict__ -- bnb constructors - # reject it when accelerate re-creates params during dispatch. + # bnb constructors reject _is_hf_initialized; strip before dispatch. _extra_keys = ("_is_hf_initialized",) _stripped = [] for _, param in model.named_parameters(): From 544195168ca2fad8175dbfa3cb3659a22ce23f50 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 16 Apr 2026 15:35:09 +0000 Subject: [PATCH 10/21] style: shorten call-site comment in vision.py --- unsloth/models/vision.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index 85aa4b62b9..ba23a575c8 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -961,11 +961,7 @@ class FastBaseModel: # attn_implementation = attn_implementation, **kwargs, ) - # Attach AlignDevicesHook for bnb multi-device / non-default-device - # loads. The bnb loading path places weights on CUDA devices but - # never calls dispatch_model, so no hooks are installed. Without - # hooks, any cross-device module call crashes at forward time with - # "Expected all tensors to be on the same device". + # Attach dispatch hooks for bnb multi-device loads. _attach_bnb_multidevice_hooks( model, load_in_4bit = load_in_4bit, From 4edb8943c2d387b0f28079e8352e2ee752e2abb2 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 16 Apr 2026 15:58:59 +0000 Subject: [PATCH 11/21] Fix review findings for PR #15 --- unsloth/models/llama.py | 11 ++++++++++- unsloth/models/vision.py | 18 +++++++++++------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/unsloth/models/llama.py b/unsloth/models/llama.py index cc021f7b97..7fb56f46e7 100644 --- a/unsloth/models/llama.py +++ b/unsloth/models/llama.py @@ -2478,6 +2478,15 @@ class FastLlamaModel: and not _head.weight.is_floating_point() ): _head.to(dtype) + from unsloth.models.vision import _attach_bnb_multidevice_hooks + + _attach_bnb_multidevice_hooks( + model, + load_in_4bit = load_in_4bit, + load_in_8bit = kwargs.get("load_in_8bit", False), + offload_embedding = False, + fast_inference = False, + ) elif not fast_inference: model = AutoModelForCausalLM.from_pretrained( model_name, @@ -2496,7 +2505,7 @@ class FastLlamaModel: _attach_bnb_multidevice_hooks( model, load_in_4bit = load_in_4bit, - load_in_8bit = False, + load_in_8bit = kwargs.get("load_in_8bit", False), offload_embedding = False, fast_inference = False, ) diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index 1bdfa9b583..f67622b5da 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -75,6 +75,7 @@ import functools import os import gc import math +import warnings from typing import Optional, Tuple, List, Union import re, inspect, sys import contextlib @@ -107,7 +108,9 @@ def _infer_device_map_from_loaded_model(model): if not params: bufs = list(module.named_buffers()) if bufs: - device_map[prefix] = bufs[0][1].device + buf_devs = {b.device for _, b in bufs} + if len(buf_devs) == 1: + device_map[prefix] = next(iter(buf_devs)) return devices = {p.device for _, p in params} if len(devices) == 1: @@ -154,8 +157,6 @@ def _attach_bnb_multidevice_hooks( if hasattr(p, "device") and p.device.type == "cuda" } except Exception as exc: - import warnings - warnings.warn( "Unsloth: Failed to determine CUDA devices from model parameters, " f"so multi-GPU hooks cannot be attached. ({type(exc).__name__}: {exc})", @@ -199,20 +200,23 @@ def _attach_bnb_multidevice_hooks( } # force_hooks=True: install hooks even for single-device maps. - dispatch_model(model, device_map = device_map_int, force_hooks = True) + dispatch_model( + model, + device_map = device_map_int, + skip_keys = getattr(model, "_skip_keys_device_placement", None), + force_hooks = True, + ) desc = f"{len(inferred_map)} block(s) across {len(cuda_devs)} device(s)" finally: # Restore stripped keys. for param, key, val in _stripped: param.__dict__[key] = val - print( + logger.info( f"Unsloth: Attached accelerate AlignDevicesHook ({desc}) " f"for bnb multi-GPU inference." ) except Exception as exc: - import warnings - warnings.warn( f"Unsloth: Could not attach multi-device dispatch hooks automatically " f"({type(exc).__name__}: {exc}). " From 29ab0fab745cfcd105cc25629f66b05bfd81b24e Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 16 Apr 2026 16:32:25 +0000 Subject: [PATCH 12/21] Fix review findings for PR #15 --- unsloth/models/llama.py | 6 +---- unsloth/models/vision.py | 47 +++++++++++++++++++--------------------- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/unsloth/models/llama.py b/unsloth/models/llama.py index 7fb56f46e7..5a49ca7d70 100644 --- a/unsloth/models/llama.py +++ b/unsloth/models/llama.py @@ -76,7 +76,7 @@ from transformers.modeling_attn_mask_utils import ( ) from ..kernels import * from ..tokenizer_utils import * -from .vision import FastBaseModel +from .vision import FastBaseModel, _attach_bnb_multidevice_hooks # Final patching code from transformers.models.llama.modeling_llama import ( @@ -2478,8 +2478,6 @@ class FastLlamaModel: and not _head.weight.is_floating_point() ): _head.to(dtype) - from unsloth.models.vision import _attach_bnb_multidevice_hooks - _attach_bnb_multidevice_hooks( model, load_in_4bit = load_in_4bit, @@ -2500,8 +2498,6 @@ class FastLlamaModel: **kwargs, ) # Attach dispatch hooks for bnb multi-device loads. - from unsloth.models.vision import _attach_bnb_multidevice_hooks - _attach_bnb_multidevice_hooks( model, load_in_4bit = load_in_4bit, diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index f67622b5da..92c6728c5c 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -104,28 +104,24 @@ def _infer_device_map_from_loaded_model(model): device_map = {} def _assign(module, prefix): - params = list(module.named_parameters(remove_duplicate = False)) - if not params: - bufs = list(module.named_buffers()) - if bufs: - buf_devs = {b.device for _, b in bufs} - if len(buf_devs) == 1: - device_map[prefix] = next(iter(buf_devs)) + subtree_devs = { + p.device for _, p in module.named_parameters(remove_duplicate = False) + } | {b.device for _, b in module.named_buffers()} + if not subtree_devs: return - devices = {p.device for _, p in params} - if len(devices) == 1: - device_map[prefix] = next(iter(devices)) - else: - for child_name, child in module.named_children(): - child_prefix = f"{prefix}.{child_name}" if prefix else child_name - _assign(child, child_prefix) - for pname, param in module.named_parameters(remove_duplicate = False): - if "." not in pname: - full = f"{prefix}.{pname}" if prefix else pname - if not any( - full == k or full.startswith(k + ".") for k in device_map - ): - device_map[full] = param.device + if len(subtree_devs) == 1: + device_map[prefix] = next(iter(subtree_devs)) + return + for child_name, child in module.named_children(): + child_prefix = f"{prefix}.{child_name}" if prefix else child_name + _assign(child, child_prefix) + local_devs = { + p.device for _, p in module.named_parameters( + recurse = False, remove_duplicate = False, + ) + } | {b.device for _, b in module.named_buffers(recurse = False)} + if local_devs and len(local_devs) == 1: + device_map[prefix] = next(iter(local_devs)) _assign(model, "") if "" in device_map and len(device_map) > 1: @@ -151,25 +147,26 @@ def _attach_bnb_multidevice_hooks( return # already dispatched try: - cuda_devs = { + all_devs = { p.device for p in model.parameters() - if hasattr(p, "device") and p.device.type == "cuda" + if hasattr(p, "device") } except Exception as exc: warnings.warn( - "Unsloth: Failed to determine CUDA devices from model parameters, " + "Unsloth: Failed to determine device placement from model parameters, " f"so multi-GPU hooks cannot be attached. ({type(exc).__name__}: {exc})", RuntimeWarning, stacklevel = 2, ) return + cuda_devs = {d for d in all_devs if d.type == "cuda"} if not cuda_devs: return default_cuda = torch.device("cuda", 0) - if cuda_devs == {default_cuda}: + if all_devs == {default_cuda}: return try: From 5e8e4487ed28cdca8d2e61306dfaf42bcab1c2b3 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 16 Apr 2026 17:01:46 +0000 Subject: [PATCH 13/21] Fix review findings for PR #15 --- unsloth/models/llama.py | 2 +- unsloth/models/vision.py | 30 ++++++++++++++++++++---------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/unsloth/models/llama.py b/unsloth/models/llama.py index 5a49ca7d70..5088c6f8a0 100644 --- a/unsloth/models/llama.py +++ b/unsloth/models/llama.py @@ -2483,7 +2483,7 @@ class FastLlamaModel: load_in_4bit = load_in_4bit, load_in_8bit = kwargs.get("load_in_8bit", False), offload_embedding = False, - fast_inference = False, + fast_inference = fast_inference, ) elif not fast_inference: model = AutoModelForCausalLM.from_pretrained( diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index 92c6728c5c..ffc47d813a 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -106,8 +106,17 @@ def _infer_device_map_from_loaded_model(model): def _assign(module, prefix): subtree_devs = { p.device for _, p in module.named_parameters(remove_duplicate = False) - } | {b.device for _, b in module.named_buffers()} + } if not subtree_devs: + bufs = list(module.named_buffers()) + if bufs: + buf_devs = {b.device for _, b in bufs} + if len(buf_devs) == 1: + device_map[prefix] = next(iter(buf_devs)) + else: + for child_name, child in module.named_children(): + child_prefix = f"{prefix}.{child_name}" if prefix else child_name + _assign(child, child_prefix) return if len(subtree_devs) == 1: device_map[prefix] = next(iter(subtree_devs)) @@ -119,13 +128,11 @@ def _infer_device_map_from_loaded_model(model): p.device for _, p in module.named_parameters( recurse = False, remove_duplicate = False, ) - } | {b.device for _, b in module.named_buffers(recurse = False)} + } if local_devs and len(local_devs) == 1: device_map[prefix] = next(iter(local_devs)) _assign(model, "") - if "" in device_map and len(device_map) > 1: - device_map.pop("") return device_map @@ -139,7 +146,14 @@ def _attach_bnb_multidevice_hooks( """ if fast_inference: return - if not (load_in_4bit or load_in_8bit): + is_bnb = ( + load_in_4bit + or load_in_8bit + or getattr(model, "is_loaded_in_4bit", False) + or getattr(model, "is_loaded_in_8bit", False) + or getattr(model, "quantization_method", None) == "bitsandbytes" + ) + if not is_bnb: return if offload_embedding: return @@ -147,11 +161,7 @@ def _attach_bnb_multidevice_hooks( return # already dispatched try: - all_devs = { - p.device - for p in model.parameters() - if hasattr(p, "device") - } + all_devs = {p.device for p in model.parameters()} except Exception as exc: warnings.warn( "Unsloth: Failed to determine device placement from model parameters, " From 7b324a63a1c4427e6d867346eb6f84d768fa6984 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 16 Apr 2026 17:29:23 +0000 Subject: [PATCH 14/21] Fix review findings for PR #15 --- unsloth/models/llama.py | 2 +- unsloth/models/vision.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/unsloth/models/llama.py b/unsloth/models/llama.py index 5088c6f8a0..c27a0f5382 100644 --- a/unsloth/models/llama.py +++ b/unsloth/models/llama.py @@ -2503,7 +2503,7 @@ class FastLlamaModel: load_in_4bit = load_in_4bit, load_in_8bit = kwargs.get("load_in_8bit", False), offload_embedding = False, - fast_inference = False, + fast_inference = fast_inference, ) model.fast_generate = make_fast_generate_wrapper(model.generate) model.fast_generate_batches = None diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index ffc47d813a..030db49168 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -207,9 +207,16 @@ def _attach_bnb_multidevice_hooks( } # force_hooks=True: install hooks even for single-device maps. + main_device = device_map_int.get("") + if main_device in (None, "cpu", "disk"): + main_device = next( + (d for d in device_map_int.values() if d not in ("cpu", "disk")), + None, + ) dispatch_model( model, device_map = device_map_int, + main_device = main_device, skip_keys = getattr(model, "_skip_keys_device_placement", None), force_hooks = True, ) From d1461ccc099d4300db285ee91285aee54f135d19 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 16 Apr 2026 18:02:38 +0000 Subject: [PATCH 15/21] Add review tests for PR #5053 --- test_attach_combined_load_flags_detection.py | 76 ++++++++++++++++++ test_attach_detects_is_loaded_in_8bit.py | 77 +++++++++++++++++++ test_attach_device_map_disk_entries.py | 81 ++++++++++++++++++++ test_attach_no_side_effects_on_early_exit.py | 69 +++++++++++++++++ test_attach_restore_on_dispatch_exception.py | 70 +++++++++++++++++ test_infer_params_and_buffers_same_module.py | 76 ++++++++++++++++++ test_infer_three_level_deep_split.py | 80 +++++++++++++++++++ test_infer_tied_params.py | 65 ++++++++++++++++ test_infer_xpu_device_type_recursion.py | 79 +++++++++++++++++++ test_vision_fastbasemodel_calls_helper.py | 36 +++++++++ 10 files changed, 709 insertions(+) create mode 100644 test_attach_combined_load_flags_detection.py create mode 100644 test_attach_detects_is_loaded_in_8bit.py create mode 100644 test_attach_device_map_disk_entries.py create mode 100644 test_attach_no_side_effects_on_early_exit.py create mode 100644 test_attach_restore_on_dispatch_exception.py create mode 100644 test_infer_params_and_buffers_same_module.py create mode 100644 test_infer_three_level_deep_split.py create mode 100644 test_infer_tied_params.py create mode 100644 test_infer_xpu_device_type_recursion.py create mode 100644 test_vision_fastbasemodel_calls_helper.py diff --git a/test_attach_combined_load_flags_detection.py b/test_attach_combined_load_flags_detection.py new file mode 100644 index 0000000000..bf298f13fc --- /dev/null +++ b/test_attach_combined_load_flags_detection.py @@ -0,0 +1,76 @@ +import ast +from pathlib import Path +import logging +import warnings as _warnings +import torch + + +def _find_vision(): + for p in [ + Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", + Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", + Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + ]: + if p.exists(): + return p + raise FileNotFoundError("vision.py not found") + + +def _load_fns(): + tree = ast.parse(_find_vision().read_text()) + ns = {"torch": torch, "warnings": _warnings, "logger": logging.getLogger("test")} + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name in { + "_infer_device_map_from_loaded_model", + "_attach_bnb_multidevice_hooks", + }: + exec(compile(ast.Module(body=[node], type_ignores=[]), str(_find_vision()), "exec"), ns) + return ns["_infer_device_map_from_loaded_model"], ns["_attach_bnb_multidevice_hooks"] + + +class _P: + def __init__(self, dev): + self.device = torch.device(dev) if isinstance(dev, str) else dev + + +class _FakeMod: + def __init__(self, params=None, buffers=None, children=None, hf_device_map=None): + self._p = list(params or []) + self._b = list(buffers or []) + self._c = list(children or []) + self.hf_device_map = hf_device_map + + def named_parameters(self, recurse=True, remove_duplicate=False): + for n, d in self._p: + yield n, _P(d) + if recurse: + for cn, cm in self._c: + for pn, pp in cm.named_parameters(recurse=True, remove_duplicate=remove_duplicate): + yield f"{cn}.{pn}", pp + + def parameters(self, recurse=True): + for _, p in self.named_parameters(recurse=recurse): + yield p + + def named_buffers(self, recurse=True): + for n, d in self._b: + yield n, _P(d) + if recurse: + for cn, cm in self._c: + for bn, bb in cm.named_buffers(recurse=True): + yield f"{cn}.{bn}", bb + + def named_children(self): + yield from self._c + + +def test_attach_load_in_4bit_bool_alone_activates(monkeypatch): + """Minimal contract: when only load_in_4bit=True is passed, the helper + activates without requiring model.is_loaded_in_4bit to also be True.""" + import accelerate + called = {"n": 0} + monkeypatch.setattr(accelerate, "dispatch_model", lambda *a, **kw: called.__setitem__("n", called["n"] + 1)) + _, attach = _load_fns() + m = _FakeMod(params=[("w", "cuda:1")]) # no is_loaded_in_* attrs set + attach(m, load_in_4bit=True, load_in_8bit=False, offload_embedding=False, fast_inference=False) + assert called["n"] == 1 diff --git a/test_attach_detects_is_loaded_in_8bit.py b/test_attach_detects_is_loaded_in_8bit.py new file mode 100644 index 0000000000..0d2b86a987 --- /dev/null +++ b/test_attach_detects_is_loaded_in_8bit.py @@ -0,0 +1,77 @@ +import ast +from pathlib import Path +import logging +import warnings as _warnings +import torch + + +def _find_vision(): + for p in [ + Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", + Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", + Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + ]: + if p.exists(): + return p + raise FileNotFoundError("vision.py not found") + + +def _load_fns(): + tree = ast.parse(_find_vision().read_text()) + ns = {"torch": torch, "warnings": _warnings, "logger": logging.getLogger("test")} + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name in { + "_infer_device_map_from_loaded_model", + "_attach_bnb_multidevice_hooks", + }: + exec(compile(ast.Module(body=[node], type_ignores=[]), str(_find_vision()), "exec"), ns) + return ns["_infer_device_map_from_loaded_model"], ns["_attach_bnb_multidevice_hooks"] + + +class _P: + def __init__(self, dev): + self.device = torch.device(dev) if isinstance(dev, str) else dev + + +class _FakeMod: + def __init__(self, params=None, buffers=None, children=None, hf_device_map=None): + self._p = list(params or []) + self._b = list(buffers or []) + self._c = list(children or []) + self.hf_device_map = hf_device_map + + def named_parameters(self, recurse=True, remove_duplicate=False): + for n, d in self._p: + yield n, _P(d) + if recurse: + for cn, cm in self._c: + for pn, pp in cm.named_parameters(recurse=True, remove_duplicate=remove_duplicate): + yield f"{cn}.{pn}", pp + + def parameters(self, recurse=True): + for _, p in self.named_parameters(recurse=recurse): + yield p + + def named_buffers(self, recurse=True): + for n, d in self._b: + yield n, _P(d) + if recurse: + for cn, cm in self._c: + for bn, bb in cm.named_buffers(recurse=True): + yield f"{cn}.{bn}", bb + + def named_children(self): + yield from self._c + + +def test_attach_detects_bnb_via_is_loaded_in_8bit(monkeypatch): + """8-bit loads via quantization_config zero out load_in_*bit booleans; + detection must still fire via model.is_loaded_in_8bit attribute.""" + import accelerate + called = {"n": 0} + monkeypatch.setattr(accelerate, "dispatch_model", lambda *a, **kw: called.__setitem__("n", called["n"] + 1)) + _, attach = _load_fns() + m = _FakeMod(params=[("w", "cuda:1")]) + m.is_loaded_in_8bit = True + attach(m, load_in_4bit=False, load_in_8bit=False, offload_embedding=False, fast_inference=False) + assert called["n"] == 1 diff --git a/test_attach_device_map_disk_entries.py b/test_attach_device_map_disk_entries.py new file mode 100644 index 0000000000..0b9ec56b72 --- /dev/null +++ b/test_attach_device_map_disk_entries.py @@ -0,0 +1,81 @@ +import ast +from pathlib import Path +import logging +import warnings as _warnings +import torch + + +def _find_vision(): + for p in [ + Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", + Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", + Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + ]: + if p.exists(): + return p + raise FileNotFoundError("vision.py not found") + + +def _load_fns(): + tree = ast.parse(_find_vision().read_text()) + ns = {"torch": torch, "warnings": _warnings, "logger": logging.getLogger("test")} + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name in { + "_infer_device_map_from_loaded_model", + "_attach_bnb_multidevice_hooks", + }: + exec(compile(ast.Module(body=[node], type_ignores=[]), str(_find_vision()), "exec"), ns) + return ns["_infer_device_map_from_loaded_model"], ns["_attach_bnb_multidevice_hooks"] + + +class _P: + def __init__(self, dev): + self.device = torch.device(dev) if isinstance(dev, str) else dev + + +class _FakeMod: + def __init__(self, params=None, buffers=None, children=None, hf_device_map=None): + self._p = list(params or []) + self._b = list(buffers or []) + self._c = list(children or []) + self.hf_device_map = hf_device_map + + def named_parameters(self, recurse=True, remove_duplicate=False): + for n, d in self._p: + yield n, _P(d) + if recurse: + for cn, cm in self._c: + for pn, pp in cm.named_parameters(recurse=True, remove_duplicate=remove_duplicate): + yield f"{cn}.{pn}", pp + + def parameters(self, recurse=True): + for _, p in self.named_parameters(recurse=recurse): + yield p + + def named_buffers(self, recurse=True): + for n, d in self._b: + yield n, _P(d) + if recurse: + for cn, cm in self._c: + for bn, bb in cm.named_buffers(recurse=True): + yield f"{cn}.{bn}", bb + + def named_children(self): + yield from self._c + + +def test_attach_main_device_skips_cpu_and_disk_candidates(monkeypatch): + """When inferred_map values mix cpu + gpu, main_device fallback must skip + non-device entries. Verifies the iter-4 `d not in ("cpu", "disk")` filter + handles both string constants.""" + import accelerate + rec = {} + monkeypatch.setattr(accelerate, "dispatch_model", lambda model, **kw: rec.update(kw)) + _, attach = _load_fns() + # First entry is cpu; fallback must find the cuda:1 entry instead. + a = _FakeMod(params=[("w", "cpu")]) + b = _FakeMod(params=[("w", "cuda:1")]) + m = _FakeMod(children=[("a", a), ("b", b)]) + attach(m, load_in_4bit=True, load_in_8bit=False, offload_embedding=False, fast_inference=False) + md = rec.get("main_device") + assert md == 1, f"main_device must skip cpu/disk strings, got {md!r}" diff --git a/test_attach_no_side_effects_on_early_exit.py b/test_attach_no_side_effects_on_early_exit.py new file mode 100644 index 0000000000..c38c2c4186 --- /dev/null +++ b/test_attach_no_side_effects_on_early_exit.py @@ -0,0 +1,69 @@ +import ast +from pathlib import Path +import logging +import warnings as _warnings +import torch + + +def _find_vision(): + for p in [ + Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", + Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", + Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + ]: + if p.exists(): + return p + raise FileNotFoundError("vision.py not found") + + +def _load_fns(): + tree = ast.parse(_find_vision().read_text()) + ns = {"torch": torch, "warnings": _warnings, "logger": logging.getLogger("test")} + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name in { + "_infer_device_map_from_loaded_model", + "_attach_bnb_multidevice_hooks", + }: + exec(compile(ast.Module(body=[node], type_ignores=[]), str(_find_vision()), "exec"), ns) + return ns["_infer_device_map_from_loaded_model"], ns["_attach_bnb_multidevice_hooks"] + + +class _P: + def __init__(self, dev): + self.device = torch.device(dev) if isinstance(dev, str) else dev + + +class _ObservableMod: + """Tracks whether any mutation happened on its parameters.""" + def __init__(self, params): + self._p = [(n, _P(d)) for n, d in params] + self.hf_device_map = None + + def named_parameters(self, recurse=True, remove_duplicate=False): + for n, p in self._p: + yield n, p + + def parameters(self, recurse=True): + for _, p in self._p: + yield p + + def named_buffers(self, recurse=True): + return iter([]) + + def named_children(self): + return iter([]) + + +def test_early_exit_does_not_strip_params(monkeypatch): + """When a guard triggers (fast_inference=True), the helper must return + before touching any parameter attributes (strip loop is never entered).""" + import accelerate + monkeypatch.setattr(accelerate, "dispatch_model", lambda *a, **kw: None) + _, attach = _load_fns() + m = _ObservableMod([("w", "cuda:1")]) + p = m._p[0][1] + p._is_hf_initialized = "original" + p._other_attr = "keep" + attach(m, load_in_4bit=True, load_in_8bit=False, offload_embedding=False, fast_inference=True) + assert p._is_hf_initialized == "original" + assert p._other_attr == "keep" diff --git a/test_attach_restore_on_dispatch_exception.py b/test_attach_restore_on_dispatch_exception.py new file mode 100644 index 0000000000..2a184fbb5e --- /dev/null +++ b/test_attach_restore_on_dispatch_exception.py @@ -0,0 +1,70 @@ +import ast +from pathlib import Path +import logging +import warnings as _warnings +import torch + + +def _find_vision(): + for p in [ + Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", + Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", + Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + ]: + if p.exists(): + return p + raise FileNotFoundError("vision.py not found") + + +def _load_fns(): + tree = ast.parse(_find_vision().read_text()) + ns = {"torch": torch, "warnings": _warnings, "logger": logging.getLogger("test")} + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name in { + "_infer_device_map_from_loaded_model", + "_attach_bnb_multidevice_hooks", + }: + exec(compile(ast.Module(body=[node], type_ignores=[]), str(_find_vision()), "exec"), ns) + return ns["_infer_device_map_from_loaded_model"], ns["_attach_bnb_multidevice_hooks"] + + +class _P: + def __init__(self, dev): + self.device = torch.device(dev) if isinstance(dev, str) else dev + + +class _TrackMod: + def __init__(self, params): + self._p = [(n, _P(d)) for n, d in params] + self.hf_device_map = None + + def named_parameters(self, recurse=True, remove_duplicate=False): + for n, p in self._p: + yield n, p + + def parameters(self, recurse=True): + for _, p in self._p: + yield p + + def named_buffers(self, recurse=True): + return iter([]) + + def named_children(self): + return iter([]) + + +def test_attach_restores_is_hf_initialized_after_dispatch_raises(monkeypatch): + """If dispatch_model raises, the inner finally must still restore the + stripped _is_hf_initialized attribute on every param.""" + import accelerate + def boom(*a, **kw): + raise RuntimeError("dispatch blew up") + monkeypatch.setattr(accelerate, "dispatch_model", boom) + _, attach = _load_fns() + m = _TrackMod([("w", "cuda:1")]) + p = next(iter(m._p))[1] + p._is_hf_initialized = True + with _warnings.catch_warnings(): + _warnings.simplefilter("ignore") + attach(m, load_in_4bit=True, load_in_8bit=False, offload_embedding=False, fast_inference=False) + assert p.__dict__.get("_is_hf_initialized") is True diff --git a/test_infer_params_and_buffers_same_module.py b/test_infer_params_and_buffers_same_module.py new file mode 100644 index 0000000000..8876eea7c9 --- /dev/null +++ b/test_infer_params_and_buffers_same_module.py @@ -0,0 +1,76 @@ +import ast +from pathlib import Path +import logging +import warnings as _warnings +import torch + + +def _find_vision(): + for p in [ + Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", + Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", + Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + ]: + if p.exists(): + return p + raise FileNotFoundError("vision.py not found") + + +def _load_fns(): + tree = ast.parse(_find_vision().read_text()) + ns = {"torch": torch, "warnings": _warnings, "logger": logging.getLogger("test")} + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name in { + "_infer_device_map_from_loaded_model", + "_attach_bnb_multidevice_hooks", + }: + exec(compile(ast.Module(body=[node], type_ignores=[]), str(_find_vision()), "exec"), ns) + return ns["_infer_device_map_from_loaded_model"], ns["_attach_bnb_multidevice_hooks"] + + +class _P: + def __init__(self, dev): + self.device = torch.device(dev) if isinstance(dev, str) else dev + + +class _FakeMod: + def __init__(self, params=None, buffers=None, children=None, hf_device_map=None): + self._p = list(params or []) + self._b = list(buffers or []) + self._c = list(children or []) + self.hf_device_map = hf_device_map + + def named_parameters(self, recurse=True, remove_duplicate=False): + for n, d in self._p: + yield n, _P(d) + if recurse: + for cn, cm in self._c: + for pn, pp in cm.named_parameters(recurse=True, remove_duplicate=remove_duplicate): + yield f"{cn}.{pn}", pp + + def parameters(self, recurse=True): + for _, p in self.named_parameters(recurse=recurse): + yield p + + def named_buffers(self, recurse=True): + for n, d in self._b: + yield n, _P(d) + if recurse: + for cn, cm in self._c: + for bn, bb in cm.named_buffers(recurse=True): + yield f"{cn}.{bn}", bb + + def named_children(self): + yield from self._c + + +def test_infer_module_with_both_params_and_buffers(): + """A leaf carrying BOTH a param and a buffer on the same device collapses + to a single entry; the buffer must not confuse the single-device path.""" + infer, _ = _load_fns() + m = _FakeMod( + params=[("weight", "cuda:1")], + buffers=[("running_mean", "cuda:1"), ("running_var", "cuda:1")], + ) + dm = infer(m) + assert dm == {"": torch.device("cuda", 1)} diff --git a/test_infer_three_level_deep_split.py b/test_infer_three_level_deep_split.py new file mode 100644 index 0000000000..855674a484 --- /dev/null +++ b/test_infer_three_level_deep_split.py @@ -0,0 +1,80 @@ +import ast +from pathlib import Path +import logging +import warnings as _warnings +import torch + + +def _find_vision(): + for p in [ + Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", + Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", + Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + ]: + if p.exists(): + return p + raise FileNotFoundError("vision.py not found") + + +def _load_fns(): + tree = ast.parse(_find_vision().read_text()) + ns = {"torch": torch, "warnings": _warnings, "logger": logging.getLogger("test")} + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name in { + "_infer_device_map_from_loaded_model", + "_attach_bnb_multidevice_hooks", + }: + exec(compile(ast.Module(body=[node], type_ignores=[]), str(_find_vision()), "exec"), ns) + return ns["_infer_device_map_from_loaded_model"], ns["_attach_bnb_multidevice_hooks"] + + +class _P: + def __init__(self, dev): + self.device = torch.device(dev) if isinstance(dev, str) else dev + + +class _FakeMod: + def __init__(self, params=None, buffers=None, children=None, hf_device_map=None): + self._p = list(params or []) + self._b = list(buffers or []) + self._c = list(children or []) + self.hf_device_map = hf_device_map + + def named_parameters(self, recurse=True, remove_duplicate=False): + for n, d in self._p: + yield n, _P(d) + if recurse: + for cn, cm in self._c: + for pn, pp in cm.named_parameters(recurse=True, remove_duplicate=remove_duplicate): + yield f"{cn}.{pn}", pp + + def parameters(self, recurse=True): + for _, p in self.named_parameters(recurse=recurse): + yield p + + def named_buffers(self, recurse=True): + for n, d in self._b: + yield n, _P(d) + if recurse: + for cn, cm in self._c: + for bn, bb in cm.named_buffers(recurse=True): + yield f"{cn}.{bn}", bb + + def named_children(self): + yield from self._c + + +def test_infer_three_level_deep_mixed(): + """Split at the third level of nesting: the algorithm must recurse deep + enough to distinguish grandchildren on different devices.""" + infer, _ = _load_fns() + g1 = _FakeMod(params=[("w", "cuda:0")]) + g2 = _FakeMod(params=[("w", "cuda:1")]) + level2 = _FakeMod(children=[("g1", g1), ("g2", g2)]) + level1 = _FakeMod(children=[("l2", level2)]) + root = _FakeMod(children=[("l1", level1)]) + dm = infer(root) + assert dm.get("l1.l2.g1") == torch.device("cuda", 0) + assert dm.get("l1.l2.g2") == torch.device("cuda", 1) + # Intermediate levels that are mixed must NOT collapse prematurely + assert "l1" not in dm or len({dm.get("l1"), dm.get("l1.l2.g1")}) > 1 diff --git a/test_infer_tied_params.py b/test_infer_tied_params.py new file mode 100644 index 0000000000..7ae0dbdbee --- /dev/null +++ b/test_infer_tied_params.py @@ -0,0 +1,65 @@ +import ast +from pathlib import Path +import logging +import warnings as _warnings +import torch + + +def _find_vision(): + for p in [ + Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", + Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", + Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + ]: + if p.exists(): + return p + raise FileNotFoundError("vision.py not found") + + +def _load_fns(): + tree = ast.parse(_find_vision().read_text()) + ns = {"torch": torch, "warnings": _warnings, "logger": logging.getLogger("test")} + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name in { + "_infer_device_map_from_loaded_model", + "_attach_bnb_multidevice_hooks", + }: + exec(compile(ast.Module(body=[node], type_ignores=[]), str(_find_vision()), "exec"), ns) + return ns["_infer_device_map_from_loaded_model"], ns["_attach_bnb_multidevice_hooks"] + + +class _P: + def __init__(self, dev): + self.device = torch.device(dev) if isinstance(dev, str) else dev + + +class _TiedMod: + """Emits the same parameter object under two different names to simulate + tied weights (lm_head.weight == embed.weight). With remove_duplicate=False + we yield both names; devices unioned must still be a single device.""" + def __init__(self, dev): + self._shared = _P(dev) + self.hf_device_map = None + + def named_parameters(self, recurse=True, remove_duplicate=False): + yield "embed.weight", self._shared + if not remove_duplicate: + yield "lm_head.weight", self._shared + + def parameters(self, recurse=True): + yield self._shared + + def named_buffers(self, recurse=True): + return iter([]) + + def named_children(self): + return iter([]) + + +def test_infer_tied_params_single_entry(): + """Tied-weight models (same Parameter yielded twice under different names) + must still collapse to a single-device map entry.""" + infer, _ = _load_fns() + m = _TiedMod("cuda:1") + dm = infer(m) + assert dm == {"": torch.device("cuda", 1)} diff --git a/test_infer_xpu_device_type_recursion.py b/test_infer_xpu_device_type_recursion.py new file mode 100644 index 0000000000..1e319b97e8 --- /dev/null +++ b/test_infer_xpu_device_type_recursion.py @@ -0,0 +1,79 @@ +import ast +from pathlib import Path +import logging +import warnings as _warnings +import torch + + +def _find_vision(): + for p in [ + Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", + Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", + Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + ]: + if p.exists(): + return p + raise FileNotFoundError("vision.py not found") + + +def _load_fns(): + tree = ast.parse(_find_vision().read_text()) + ns = {"torch": torch, "warnings": _warnings, "logger": logging.getLogger("test")} + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name in { + "_infer_device_map_from_loaded_model", + "_attach_bnb_multidevice_hooks", + }: + exec(compile(ast.Module(body=[node], type_ignores=[]), str(_find_vision()), "exec"), ns) + return ns["_infer_device_map_from_loaded_model"], ns["_attach_bnb_multidevice_hooks"] + + +class _P: + def __init__(self, dev): + self.device = dev if isinstance(dev, torch.device) else torch.device(dev) + + +class _FakeMod: + def __init__(self, params=None, buffers=None, children=None, hf_device_map=None): + self._p = list(params or []) + self._b = list(buffers or []) + self._c = list(children or []) + self.hf_device_map = hf_device_map + + def named_parameters(self, recurse=True, remove_duplicate=False): + for n, d in self._p: + yield n, _P(d) + if recurse: + for cn, cm in self._c: + for pn, pp in cm.named_parameters(recurse=True, remove_duplicate=remove_duplicate): + yield f"{cn}.{pn}", pp + + def parameters(self, recurse=True): + for _, p in self.named_parameters(recurse=recurse): + yield p + + def named_buffers(self, recurse=True): + for n, d in self._b: + yield n, _P(d) + if recurse: + for cn, cm in self._c: + for bn, bb in cm.named_buffers(recurse=True): + yield f"{cn}.{bn}", bb + + def named_children(self): + yield from self._c + + +def test_infer_handles_xpu_device_recursion(): + """XPU / custom-device types must recurse and be assigned correctly. The + infer function is device-type agnostic and should treat different xpu + indices as distinct devices for map-building purposes.""" + infer, _ = _load_fns() + # xpu:0 and xpu:1 — avoids any cuda-specific codepath in infer + a = _FakeMod(params=[("w", torch.device("xpu", 0))]) + b = _FakeMod(params=[("w", torch.device("xpu", 1))]) + root = _FakeMod(children=[("a", a), ("b", b)]) + dm = infer(root) + assert dm.get("a") == torch.device("xpu", 0) + assert dm.get("b") == torch.device("xpu", 1) + assert "" not in dm diff --git a/test_vision_fastbasemodel_calls_helper.py b/test_vision_fastbasemodel_calls_helper.py new file mode 100644 index 0000000000..c089c5383b --- /dev/null +++ b/test_vision_fastbasemodel_calls_helper.py @@ -0,0 +1,36 @@ +import ast +from pathlib import Path + + +def _find_vision(): + for p in [ + Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", + Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", + Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + ]: + if p.exists(): + return p + raise FileNotFoundError("vision.py not found") + + +def test_vision_fastbasemodel_from_pretrained_calls_helper(): + """FastBaseModel.from_pretrained must invoke _attach_bnb_multidevice_hooks + after the underlying model load so the inference hook path is reachable + via the base vision loader, not only via llama.""" + src = _find_vision().read_text() + tree = ast.parse(src) + found = False + for cls in ast.walk(tree): + if not (isinstance(cls, ast.ClassDef) and cls.name == "FastBaseModel"): + continue + for fn in ast.walk(cls): + if not (isinstance(fn, ast.FunctionDef) and fn.name == "from_pretrained"): + continue + for node in ast.walk(fn): + if ( + isinstance(node, ast.Call) + and getattr(node.func, "id", None) == "_attach_bnb_multidevice_hooks" + ): + found = True + break + assert found, "FastBaseModel.from_pretrained must call _attach_bnb_multidevice_hooks" From 564a031e70889aa8365a76840612aa407e8bbd89 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:06:24 +0000 Subject: [PATCH 16/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test_attach_combined_load_flags_detection.py | 50 ++++++++++++++----- test_attach_detects_is_loaded_in_8bit.py | 50 ++++++++++++++----- test_attach_device_map_disk_entries.py | 52 ++++++++++++++------ test_attach_no_side_effects_on_early_exit.py | 33 ++++++++++--- test_attach_restore_on_dispatch_exception.py | 33 ++++++++++--- test_infer_params_and_buffers_same_module.py | 37 +++++++++----- test_infer_three_level_deep_split.py | 43 ++++++++++------ test_infer_tied_params.py | 24 ++++++--- test_infer_xpu_device_type_recursion.py | 39 ++++++++++----- test_vision_fastbasemodel_calls_helper.py | 11 +++-- unsloth/models/vision.py | 10 ++-- 11 files changed, 275 insertions(+), 107 deletions(-) diff --git a/test_attach_combined_load_flags_detection.py b/test_attach_combined_load_flags_detection.py index bf298f13fc..5872fa3c1a 100644 --- a/test_attach_combined_load_flags_detection.py +++ b/test_attach_combined_load_flags_detection.py @@ -9,7 +9,9 @@ def _find_vision(): for p in [ Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", - Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + Path( + "/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py" + ), ]: if p.exists(): return p @@ -24,8 +26,17 @@ def _load_fns(): "_infer_device_map_from_loaded_model", "_attach_bnb_multidevice_hooks", }: - exec(compile(ast.Module(body=[node], type_ignores=[]), str(_find_vision()), "exec"), ns) - return ns["_infer_device_map_from_loaded_model"], ns["_attach_bnb_multidevice_hooks"] + exec( + compile( + ast.Module(body = [node], type_ignores = []), + str(_find_vision()), + "exec", + ), + ns, + ) + return ns["_infer_device_map_from_loaded_model"], ns[ + "_attach_bnb_multidevice_hooks" + ] class _P: @@ -34,30 +45,32 @@ class _P: class _FakeMod: - def __init__(self, params=None, buffers=None, children=None, hf_device_map=None): + def __init__(self, params = None, buffers = None, children = None, hf_device_map = None): self._p = list(params or []) self._b = list(buffers or []) self._c = list(children or []) self.hf_device_map = hf_device_map - def named_parameters(self, recurse=True, remove_duplicate=False): + def named_parameters(self, recurse = True, remove_duplicate = False): for n, d in self._p: yield n, _P(d) if recurse: for cn, cm in self._c: - for pn, pp in cm.named_parameters(recurse=True, remove_duplicate=remove_duplicate): + for pn, pp in cm.named_parameters( + recurse = True, remove_duplicate = remove_duplicate + ): yield f"{cn}.{pn}", pp - def parameters(self, recurse=True): - for _, p in self.named_parameters(recurse=recurse): + def parameters(self, recurse = True): + for _, p in self.named_parameters(recurse = recurse): yield p - def named_buffers(self, recurse=True): + def named_buffers(self, recurse = True): for n, d in self._b: yield n, _P(d) if recurse: for cn, cm in self._c: - for bn, bb in cm.named_buffers(recurse=True): + for bn, bb in cm.named_buffers(recurse = True): yield f"{cn}.{bn}", bb def named_children(self): @@ -68,9 +81,20 @@ def test_attach_load_in_4bit_bool_alone_activates(monkeypatch): """Minimal contract: when only load_in_4bit=True is passed, the helper activates without requiring model.is_loaded_in_4bit to also be True.""" import accelerate + called = {"n": 0} - monkeypatch.setattr(accelerate, "dispatch_model", lambda *a, **kw: called.__setitem__("n", called["n"] + 1)) + monkeypatch.setattr( + accelerate, + "dispatch_model", + lambda *a, **kw: called.__setitem__("n", called["n"] + 1), + ) _, attach = _load_fns() - m = _FakeMod(params=[("w", "cuda:1")]) # no is_loaded_in_* attrs set - attach(m, load_in_4bit=True, load_in_8bit=False, offload_embedding=False, fast_inference=False) + m = _FakeMod(params = [("w", "cuda:1")]) # no is_loaded_in_* attrs set + attach( + m, + load_in_4bit = True, + load_in_8bit = False, + offload_embedding = False, + fast_inference = False, + ) assert called["n"] == 1 diff --git a/test_attach_detects_is_loaded_in_8bit.py b/test_attach_detects_is_loaded_in_8bit.py index 0d2b86a987..6e51640968 100644 --- a/test_attach_detects_is_loaded_in_8bit.py +++ b/test_attach_detects_is_loaded_in_8bit.py @@ -9,7 +9,9 @@ def _find_vision(): for p in [ Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", - Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + Path( + "/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py" + ), ]: if p.exists(): return p @@ -24,8 +26,17 @@ def _load_fns(): "_infer_device_map_from_loaded_model", "_attach_bnb_multidevice_hooks", }: - exec(compile(ast.Module(body=[node], type_ignores=[]), str(_find_vision()), "exec"), ns) - return ns["_infer_device_map_from_loaded_model"], ns["_attach_bnb_multidevice_hooks"] + exec( + compile( + ast.Module(body = [node], type_ignores = []), + str(_find_vision()), + "exec", + ), + ns, + ) + return ns["_infer_device_map_from_loaded_model"], ns[ + "_attach_bnb_multidevice_hooks" + ] class _P: @@ -34,30 +45,32 @@ class _P: class _FakeMod: - def __init__(self, params=None, buffers=None, children=None, hf_device_map=None): + def __init__(self, params = None, buffers = None, children = None, hf_device_map = None): self._p = list(params or []) self._b = list(buffers or []) self._c = list(children or []) self.hf_device_map = hf_device_map - def named_parameters(self, recurse=True, remove_duplicate=False): + def named_parameters(self, recurse = True, remove_duplicate = False): for n, d in self._p: yield n, _P(d) if recurse: for cn, cm in self._c: - for pn, pp in cm.named_parameters(recurse=True, remove_duplicate=remove_duplicate): + for pn, pp in cm.named_parameters( + recurse = True, remove_duplicate = remove_duplicate + ): yield f"{cn}.{pn}", pp - def parameters(self, recurse=True): - for _, p in self.named_parameters(recurse=recurse): + def parameters(self, recurse = True): + for _, p in self.named_parameters(recurse = recurse): yield p - def named_buffers(self, recurse=True): + def named_buffers(self, recurse = True): for n, d in self._b: yield n, _P(d) if recurse: for cn, cm in self._c: - for bn, bb in cm.named_buffers(recurse=True): + for bn, bb in cm.named_buffers(recurse = True): yield f"{cn}.{bn}", bb def named_children(self): @@ -68,10 +81,21 @@ def test_attach_detects_bnb_via_is_loaded_in_8bit(monkeypatch): """8-bit loads via quantization_config zero out load_in_*bit booleans; detection must still fire via model.is_loaded_in_8bit attribute.""" import accelerate + called = {"n": 0} - monkeypatch.setattr(accelerate, "dispatch_model", lambda *a, **kw: called.__setitem__("n", called["n"] + 1)) + monkeypatch.setattr( + accelerate, + "dispatch_model", + lambda *a, **kw: called.__setitem__("n", called["n"] + 1), + ) _, attach = _load_fns() - m = _FakeMod(params=[("w", "cuda:1")]) + m = _FakeMod(params = [("w", "cuda:1")]) m.is_loaded_in_8bit = True - attach(m, load_in_4bit=False, load_in_8bit=False, offload_embedding=False, fast_inference=False) + attach( + m, + load_in_4bit = False, + load_in_8bit = False, + offload_embedding = False, + fast_inference = False, + ) assert called["n"] == 1 diff --git a/test_attach_device_map_disk_entries.py b/test_attach_device_map_disk_entries.py index 0b9ec56b72..b06276c79f 100644 --- a/test_attach_device_map_disk_entries.py +++ b/test_attach_device_map_disk_entries.py @@ -9,7 +9,9 @@ def _find_vision(): for p in [ Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", - Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + Path( + "/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py" + ), ]: if p.exists(): return p @@ -24,8 +26,17 @@ def _load_fns(): "_infer_device_map_from_loaded_model", "_attach_bnb_multidevice_hooks", }: - exec(compile(ast.Module(body=[node], type_ignores=[]), str(_find_vision()), "exec"), ns) - return ns["_infer_device_map_from_loaded_model"], ns["_attach_bnb_multidevice_hooks"] + exec( + compile( + ast.Module(body = [node], type_ignores = []), + str(_find_vision()), + "exec", + ), + ns, + ) + return ns["_infer_device_map_from_loaded_model"], ns[ + "_attach_bnb_multidevice_hooks" + ] class _P: @@ -34,30 +45,32 @@ class _P: class _FakeMod: - def __init__(self, params=None, buffers=None, children=None, hf_device_map=None): + def __init__(self, params = None, buffers = None, children = None, hf_device_map = None): self._p = list(params or []) self._b = list(buffers or []) self._c = list(children or []) self.hf_device_map = hf_device_map - def named_parameters(self, recurse=True, remove_duplicate=False): + def named_parameters(self, recurse = True, remove_duplicate = False): for n, d in self._p: yield n, _P(d) if recurse: for cn, cm in self._c: - for pn, pp in cm.named_parameters(recurse=True, remove_duplicate=remove_duplicate): + for pn, pp in cm.named_parameters( + recurse = True, remove_duplicate = remove_duplicate + ): yield f"{cn}.{pn}", pp - def parameters(self, recurse=True): - for _, p in self.named_parameters(recurse=recurse): + def parameters(self, recurse = True): + for _, p in self.named_parameters(recurse = recurse): yield p - def named_buffers(self, recurse=True): + def named_buffers(self, recurse = True): for n, d in self._b: yield n, _P(d) if recurse: for cn, cm in self._c: - for bn, bb in cm.named_buffers(recurse=True): + for bn, bb in cm.named_buffers(recurse = True): yield f"{cn}.{bn}", bb def named_children(self): @@ -69,13 +82,22 @@ def test_attach_main_device_skips_cpu_and_disk_candidates(monkeypatch): non-device entries. Verifies the iter-4 `d not in ("cpu", "disk")` filter handles both string constants.""" import accelerate + rec = {} - monkeypatch.setattr(accelerate, "dispatch_model", lambda model, **kw: rec.update(kw)) + monkeypatch.setattr( + accelerate, "dispatch_model", lambda model, **kw: rec.update(kw) + ) _, attach = _load_fns() # First entry is cpu; fallback must find the cuda:1 entry instead. - a = _FakeMod(params=[("w", "cpu")]) - b = _FakeMod(params=[("w", "cuda:1")]) - m = _FakeMod(children=[("a", a), ("b", b)]) - attach(m, load_in_4bit=True, load_in_8bit=False, offload_embedding=False, fast_inference=False) + a = _FakeMod(params = [("w", "cpu")]) + b = _FakeMod(params = [("w", "cuda:1")]) + m = _FakeMod(children = [("a", a), ("b", b)]) + attach( + m, + load_in_4bit = True, + load_in_8bit = False, + offload_embedding = False, + fast_inference = False, + ) md = rec.get("main_device") assert md == 1, f"main_device must skip cpu/disk strings, got {md!r}" diff --git a/test_attach_no_side_effects_on_early_exit.py b/test_attach_no_side_effects_on_early_exit.py index c38c2c4186..04f9c389a9 100644 --- a/test_attach_no_side_effects_on_early_exit.py +++ b/test_attach_no_side_effects_on_early_exit.py @@ -9,7 +9,9 @@ def _find_vision(): for p in [ Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", - Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + Path( + "/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py" + ), ]: if p.exists(): return p @@ -24,8 +26,17 @@ def _load_fns(): "_infer_device_map_from_loaded_model", "_attach_bnb_multidevice_hooks", }: - exec(compile(ast.Module(body=[node], type_ignores=[]), str(_find_vision()), "exec"), ns) - return ns["_infer_device_map_from_loaded_model"], ns["_attach_bnb_multidevice_hooks"] + exec( + compile( + ast.Module(body = [node], type_ignores = []), + str(_find_vision()), + "exec", + ), + ns, + ) + return ns["_infer_device_map_from_loaded_model"], ns[ + "_attach_bnb_multidevice_hooks" + ] class _P: @@ -35,19 +46,20 @@ class _P: class _ObservableMod: """Tracks whether any mutation happened on its parameters.""" + def __init__(self, params): self._p = [(n, _P(d)) for n, d in params] self.hf_device_map = None - def named_parameters(self, recurse=True, remove_duplicate=False): + def named_parameters(self, recurse = True, remove_duplicate = False): for n, p in self._p: yield n, p - def parameters(self, recurse=True): + def parameters(self, recurse = True): for _, p in self._p: yield p - def named_buffers(self, recurse=True): + def named_buffers(self, recurse = True): return iter([]) def named_children(self): @@ -58,12 +70,19 @@ def test_early_exit_does_not_strip_params(monkeypatch): """When a guard triggers (fast_inference=True), the helper must return before touching any parameter attributes (strip loop is never entered).""" import accelerate + monkeypatch.setattr(accelerate, "dispatch_model", lambda *a, **kw: None) _, attach = _load_fns() m = _ObservableMod([("w", "cuda:1")]) p = m._p[0][1] p._is_hf_initialized = "original" p._other_attr = "keep" - attach(m, load_in_4bit=True, load_in_8bit=False, offload_embedding=False, fast_inference=True) + attach( + m, + load_in_4bit = True, + load_in_8bit = False, + offload_embedding = False, + fast_inference = True, + ) assert p._is_hf_initialized == "original" assert p._other_attr == "keep" diff --git a/test_attach_restore_on_dispatch_exception.py b/test_attach_restore_on_dispatch_exception.py index 2a184fbb5e..7e320e5e91 100644 --- a/test_attach_restore_on_dispatch_exception.py +++ b/test_attach_restore_on_dispatch_exception.py @@ -9,7 +9,9 @@ def _find_vision(): for p in [ Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", - Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + Path( + "/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py" + ), ]: if p.exists(): return p @@ -24,8 +26,17 @@ def _load_fns(): "_infer_device_map_from_loaded_model", "_attach_bnb_multidevice_hooks", }: - exec(compile(ast.Module(body=[node], type_ignores=[]), str(_find_vision()), "exec"), ns) - return ns["_infer_device_map_from_loaded_model"], ns["_attach_bnb_multidevice_hooks"] + exec( + compile( + ast.Module(body = [node], type_ignores = []), + str(_find_vision()), + "exec", + ), + ns, + ) + return ns["_infer_device_map_from_loaded_model"], ns[ + "_attach_bnb_multidevice_hooks" + ] class _P: @@ -38,15 +49,15 @@ class _TrackMod: self._p = [(n, _P(d)) for n, d in params] self.hf_device_map = None - def named_parameters(self, recurse=True, remove_duplicate=False): + def named_parameters(self, recurse = True, remove_duplicate = False): for n, p in self._p: yield n, p - def parameters(self, recurse=True): + def parameters(self, recurse = True): for _, p in self._p: yield p - def named_buffers(self, recurse=True): + def named_buffers(self, recurse = True): return iter([]) def named_children(self): @@ -57,8 +68,10 @@ def test_attach_restores_is_hf_initialized_after_dispatch_raises(monkeypatch): """If dispatch_model raises, the inner finally must still restore the stripped _is_hf_initialized attribute on every param.""" import accelerate + def boom(*a, **kw): raise RuntimeError("dispatch blew up") + monkeypatch.setattr(accelerate, "dispatch_model", boom) _, attach = _load_fns() m = _TrackMod([("w", "cuda:1")]) @@ -66,5 +79,11 @@ def test_attach_restores_is_hf_initialized_after_dispatch_raises(monkeypatch): p._is_hf_initialized = True with _warnings.catch_warnings(): _warnings.simplefilter("ignore") - attach(m, load_in_4bit=True, load_in_8bit=False, offload_embedding=False, fast_inference=False) + attach( + m, + load_in_4bit = True, + load_in_8bit = False, + offload_embedding = False, + fast_inference = False, + ) assert p.__dict__.get("_is_hf_initialized") is True diff --git a/test_infer_params_and_buffers_same_module.py b/test_infer_params_and_buffers_same_module.py index 8876eea7c9..bb9dd5bf07 100644 --- a/test_infer_params_and_buffers_same_module.py +++ b/test_infer_params_and_buffers_same_module.py @@ -9,7 +9,9 @@ def _find_vision(): for p in [ Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", - Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + Path( + "/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py" + ), ]: if p.exists(): return p @@ -24,8 +26,17 @@ def _load_fns(): "_infer_device_map_from_loaded_model", "_attach_bnb_multidevice_hooks", }: - exec(compile(ast.Module(body=[node], type_ignores=[]), str(_find_vision()), "exec"), ns) - return ns["_infer_device_map_from_loaded_model"], ns["_attach_bnb_multidevice_hooks"] + exec( + compile( + ast.Module(body = [node], type_ignores = []), + str(_find_vision()), + "exec", + ), + ns, + ) + return ns["_infer_device_map_from_loaded_model"], ns[ + "_attach_bnb_multidevice_hooks" + ] class _P: @@ -34,30 +45,32 @@ class _P: class _FakeMod: - def __init__(self, params=None, buffers=None, children=None, hf_device_map=None): + def __init__(self, params = None, buffers = None, children = None, hf_device_map = None): self._p = list(params or []) self._b = list(buffers or []) self._c = list(children or []) self.hf_device_map = hf_device_map - def named_parameters(self, recurse=True, remove_duplicate=False): + def named_parameters(self, recurse = True, remove_duplicate = False): for n, d in self._p: yield n, _P(d) if recurse: for cn, cm in self._c: - for pn, pp in cm.named_parameters(recurse=True, remove_duplicate=remove_duplicate): + for pn, pp in cm.named_parameters( + recurse = True, remove_duplicate = remove_duplicate + ): yield f"{cn}.{pn}", pp - def parameters(self, recurse=True): - for _, p in self.named_parameters(recurse=recurse): + def parameters(self, recurse = True): + for _, p in self.named_parameters(recurse = recurse): yield p - def named_buffers(self, recurse=True): + def named_buffers(self, recurse = True): for n, d in self._b: yield n, _P(d) if recurse: for cn, cm in self._c: - for bn, bb in cm.named_buffers(recurse=True): + for bn, bb in cm.named_buffers(recurse = True): yield f"{cn}.{bn}", bb def named_children(self): @@ -69,8 +82,8 @@ def test_infer_module_with_both_params_and_buffers(): to a single entry; the buffer must not confuse the single-device path.""" infer, _ = _load_fns() m = _FakeMod( - params=[("weight", "cuda:1")], - buffers=[("running_mean", "cuda:1"), ("running_var", "cuda:1")], + params = [("weight", "cuda:1")], + buffers = [("running_mean", "cuda:1"), ("running_var", "cuda:1")], ) dm = infer(m) assert dm == {"": torch.device("cuda", 1)} diff --git a/test_infer_three_level_deep_split.py b/test_infer_three_level_deep_split.py index 855674a484..f336afc190 100644 --- a/test_infer_three_level_deep_split.py +++ b/test_infer_three_level_deep_split.py @@ -9,7 +9,9 @@ def _find_vision(): for p in [ Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", - Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + Path( + "/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py" + ), ]: if p.exists(): return p @@ -24,8 +26,17 @@ def _load_fns(): "_infer_device_map_from_loaded_model", "_attach_bnb_multidevice_hooks", }: - exec(compile(ast.Module(body=[node], type_ignores=[]), str(_find_vision()), "exec"), ns) - return ns["_infer_device_map_from_loaded_model"], ns["_attach_bnb_multidevice_hooks"] + exec( + compile( + ast.Module(body = [node], type_ignores = []), + str(_find_vision()), + "exec", + ), + ns, + ) + return ns["_infer_device_map_from_loaded_model"], ns[ + "_attach_bnb_multidevice_hooks" + ] class _P: @@ -34,30 +45,32 @@ class _P: class _FakeMod: - def __init__(self, params=None, buffers=None, children=None, hf_device_map=None): + def __init__(self, params = None, buffers = None, children = None, hf_device_map = None): self._p = list(params or []) self._b = list(buffers or []) self._c = list(children or []) self.hf_device_map = hf_device_map - def named_parameters(self, recurse=True, remove_duplicate=False): + def named_parameters(self, recurse = True, remove_duplicate = False): for n, d in self._p: yield n, _P(d) if recurse: for cn, cm in self._c: - for pn, pp in cm.named_parameters(recurse=True, remove_duplicate=remove_duplicate): + for pn, pp in cm.named_parameters( + recurse = True, remove_duplicate = remove_duplicate + ): yield f"{cn}.{pn}", pp - def parameters(self, recurse=True): - for _, p in self.named_parameters(recurse=recurse): + def parameters(self, recurse = True): + for _, p in self.named_parameters(recurse = recurse): yield p - def named_buffers(self, recurse=True): + def named_buffers(self, recurse = True): for n, d in self._b: yield n, _P(d) if recurse: for cn, cm in self._c: - for bn, bb in cm.named_buffers(recurse=True): + for bn, bb in cm.named_buffers(recurse = True): yield f"{cn}.{bn}", bb def named_children(self): @@ -68,11 +81,11 @@ def test_infer_three_level_deep_mixed(): """Split at the third level of nesting: the algorithm must recurse deep enough to distinguish grandchildren on different devices.""" infer, _ = _load_fns() - g1 = _FakeMod(params=[("w", "cuda:0")]) - g2 = _FakeMod(params=[("w", "cuda:1")]) - level2 = _FakeMod(children=[("g1", g1), ("g2", g2)]) - level1 = _FakeMod(children=[("l2", level2)]) - root = _FakeMod(children=[("l1", level1)]) + g1 = _FakeMod(params = [("w", "cuda:0")]) + g2 = _FakeMod(params = [("w", "cuda:1")]) + level2 = _FakeMod(children = [("g1", g1), ("g2", g2)]) + level1 = _FakeMod(children = [("l2", level2)]) + root = _FakeMod(children = [("l1", level1)]) dm = infer(root) assert dm.get("l1.l2.g1") == torch.device("cuda", 0) assert dm.get("l1.l2.g2") == torch.device("cuda", 1) diff --git a/test_infer_tied_params.py b/test_infer_tied_params.py index 7ae0dbdbee..dbf93055b4 100644 --- a/test_infer_tied_params.py +++ b/test_infer_tied_params.py @@ -9,7 +9,9 @@ def _find_vision(): for p in [ Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", - Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + Path( + "/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py" + ), ]: if p.exists(): return p @@ -24,8 +26,17 @@ def _load_fns(): "_infer_device_map_from_loaded_model", "_attach_bnb_multidevice_hooks", }: - exec(compile(ast.Module(body=[node], type_ignores=[]), str(_find_vision()), "exec"), ns) - return ns["_infer_device_map_from_loaded_model"], ns["_attach_bnb_multidevice_hooks"] + exec( + compile( + ast.Module(body = [node], type_ignores = []), + str(_find_vision()), + "exec", + ), + ns, + ) + return ns["_infer_device_map_from_loaded_model"], ns[ + "_attach_bnb_multidevice_hooks" + ] class _P: @@ -37,19 +48,20 @@ class _TiedMod: """Emits the same parameter object under two different names to simulate tied weights (lm_head.weight == embed.weight). With remove_duplicate=False we yield both names; devices unioned must still be a single device.""" + def __init__(self, dev): self._shared = _P(dev) self.hf_device_map = None - def named_parameters(self, recurse=True, remove_duplicate=False): + def named_parameters(self, recurse = True, remove_duplicate = False): yield "embed.weight", self._shared if not remove_duplicate: yield "lm_head.weight", self._shared - def parameters(self, recurse=True): + def parameters(self, recurse = True): yield self._shared - def named_buffers(self, recurse=True): + def named_buffers(self, recurse = True): return iter([]) def named_children(self): diff --git a/test_infer_xpu_device_type_recursion.py b/test_infer_xpu_device_type_recursion.py index 1e319b97e8..63e4d17ad9 100644 --- a/test_infer_xpu_device_type_recursion.py +++ b/test_infer_xpu_device_type_recursion.py @@ -9,7 +9,9 @@ def _find_vision(): for p in [ Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", - Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + Path( + "/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py" + ), ]: if p.exists(): return p @@ -24,8 +26,17 @@ def _load_fns(): "_infer_device_map_from_loaded_model", "_attach_bnb_multidevice_hooks", }: - exec(compile(ast.Module(body=[node], type_ignores=[]), str(_find_vision()), "exec"), ns) - return ns["_infer_device_map_from_loaded_model"], ns["_attach_bnb_multidevice_hooks"] + exec( + compile( + ast.Module(body = [node], type_ignores = []), + str(_find_vision()), + "exec", + ), + ns, + ) + return ns["_infer_device_map_from_loaded_model"], ns[ + "_attach_bnb_multidevice_hooks" + ] class _P: @@ -34,30 +45,32 @@ class _P: class _FakeMod: - def __init__(self, params=None, buffers=None, children=None, hf_device_map=None): + def __init__(self, params = None, buffers = None, children = None, hf_device_map = None): self._p = list(params or []) self._b = list(buffers or []) self._c = list(children or []) self.hf_device_map = hf_device_map - def named_parameters(self, recurse=True, remove_duplicate=False): + def named_parameters(self, recurse = True, remove_duplicate = False): for n, d in self._p: yield n, _P(d) if recurse: for cn, cm in self._c: - for pn, pp in cm.named_parameters(recurse=True, remove_duplicate=remove_duplicate): + for pn, pp in cm.named_parameters( + recurse = True, remove_duplicate = remove_duplicate + ): yield f"{cn}.{pn}", pp - def parameters(self, recurse=True): - for _, p in self.named_parameters(recurse=recurse): + def parameters(self, recurse = True): + for _, p in self.named_parameters(recurse = recurse): yield p - def named_buffers(self, recurse=True): + def named_buffers(self, recurse = True): for n, d in self._b: yield n, _P(d) if recurse: for cn, cm in self._c: - for bn, bb in cm.named_buffers(recurse=True): + for bn, bb in cm.named_buffers(recurse = True): yield f"{cn}.{bn}", bb def named_children(self): @@ -70,9 +83,9 @@ def test_infer_handles_xpu_device_recursion(): indices as distinct devices for map-building purposes.""" infer, _ = _load_fns() # xpu:0 and xpu:1 — avoids any cuda-specific codepath in infer - a = _FakeMod(params=[("w", torch.device("xpu", 0))]) - b = _FakeMod(params=[("w", torch.device("xpu", 1))]) - root = _FakeMod(children=[("a", a), ("b", b)]) + a = _FakeMod(params = [("w", torch.device("xpu", 0))]) + b = _FakeMod(params = [("w", torch.device("xpu", 1))]) + root = _FakeMod(children = [("a", a), ("b", b)]) dm = infer(root) assert dm.get("a") == torch.device("xpu", 0) assert dm.get("b") == torch.device("xpu", 1) diff --git a/test_vision_fastbasemodel_calls_helper.py b/test_vision_fastbasemodel_calls_helper.py index c089c5383b..e23c75e482 100644 --- a/test_vision_fastbasemodel_calls_helper.py +++ b/test_vision_fastbasemodel_calls_helper.py @@ -6,7 +6,9 @@ def _find_vision(): for p in [ Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", - Path("/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py"), + Path( + "/mnt/disks/unslothai/ubuntu/workspace_25/github_review/unsloth-pr-5053-staging-3/unsloth/models/vision.py" + ), ]: if p.exists(): return p @@ -29,8 +31,11 @@ def test_vision_fastbasemodel_from_pretrained_calls_helper(): for node in ast.walk(fn): if ( isinstance(node, ast.Call) - and getattr(node.func, "id", None) == "_attach_bnb_multidevice_hooks" + and getattr(node.func, "id", None) + == "_attach_bnb_multidevice_hooks" ): found = True break - assert found, "FastBaseModel.from_pretrained must call _attach_bnb_multidevice_hooks" + assert ( + found + ), "FastBaseModel.from_pretrained must call _attach_bnb_multidevice_hooks" diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index 030db49168..61e0822aa5 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -115,7 +115,9 @@ def _infer_device_map_from_loaded_model(model): device_map[prefix] = next(iter(buf_devs)) else: for child_name, child in module.named_children(): - child_prefix = f"{prefix}.{child_name}" if prefix else child_name + child_prefix = ( + f"{prefix}.{child_name}" if prefix else child_name + ) _assign(child, child_prefix) return if len(subtree_devs) == 1: @@ -125,8 +127,10 @@ def _infer_device_map_from_loaded_model(model): child_prefix = f"{prefix}.{child_name}" if prefix else child_name _assign(child, child_prefix) local_devs = { - p.device for _, p in module.named_parameters( - recurse = False, remove_duplicate = False, + p.device + for _, p in module.named_parameters( + recurse = False, + remove_duplicate = False, ) } if local_devs and len(local_devs) == 1: From 9741e2ff0bd974a8904862665a3d82bdccefc520 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 20 Apr 2026 23:19:40 +0000 Subject: [PATCH 17/21] Split: keep only 10 file(s) --- unsloth/models/llama.py | 9 ++++++-- unsloth/models/vision.py | 47 ++++++++++++++++------------------------ 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/unsloth/models/llama.py b/unsloth/models/llama.py index c27a0f5382..d39f2588ef 100644 --- a/unsloth/models/llama.py +++ b/unsloth/models/llama.py @@ -76,7 +76,7 @@ from transformers.modeling_attn_mask_utils import ( ) from ..kernels import * from ..tokenizer_utils import * -from .vision import FastBaseModel, _attach_bnb_multidevice_hooks +from .vision import FastBaseModel # Final patching code from transformers.models.llama.modeling_llama import ( @@ -2478,6 +2478,9 @@ class FastLlamaModel: and not _head.weight.is_floating_point() ): _head.to(dtype) + # Attach dispatch hooks for bnb multi-device loads. + from unsloth.models.vision import _attach_bnb_multidevice_hooks + _attach_bnb_multidevice_hooks( model, load_in_4bit = load_in_4bit, @@ -2498,12 +2501,14 @@ class FastLlamaModel: **kwargs, ) # Attach dispatch hooks for bnb multi-device loads. + from unsloth.models.vision import _attach_bnb_multidevice_hooks + _attach_bnb_multidevice_hooks( model, load_in_4bit = load_in_4bit, load_in_8bit = kwargs.get("load_in_8bit", False), offload_embedding = False, - fast_inference = fast_inference, + fast_inference = False, ) model.fast_generate = make_fast_generate_wrapper(model.generate) model.fast_generate_batches = None diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index 61e0822aa5..df371e00c8 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -104,39 +104,30 @@ def _infer_device_map_from_loaded_model(model): device_map = {} def _assign(module, prefix): - subtree_devs = { - p.device for _, p in module.named_parameters(remove_duplicate = False) - } - if not subtree_devs: + params = list(module.named_parameters(remove_duplicate = False)) + if not params: bufs = list(module.named_buffers()) if bufs: - buf_devs = {b.device for _, b in bufs} - if len(buf_devs) == 1: - device_map[prefix] = next(iter(buf_devs)) - else: - for child_name, child in module.named_children(): - child_prefix = ( - f"{prefix}.{child_name}" if prefix else child_name - ) - _assign(child, child_prefix) + device_map[prefix] = bufs[0][1].device return - if len(subtree_devs) == 1: - device_map[prefix] = next(iter(subtree_devs)) - return - for child_name, child in module.named_children(): - child_prefix = f"{prefix}.{child_name}" if prefix else child_name - _assign(child, child_prefix) - local_devs = { - p.device - for _, p in module.named_parameters( - recurse = False, - remove_duplicate = False, - ) - } - if local_devs and len(local_devs) == 1: - device_map[prefix] = next(iter(local_devs)) + devices = {p.device for _, p in params} + if len(devices) == 1: + device_map[prefix] = next(iter(devices)) + else: + for child_name, child in module.named_children(): + child_prefix = f"{prefix}.{child_name}" if prefix else child_name + _assign(child, child_prefix) + for pname, param in module.named_parameters(remove_duplicate = False): + if "." not in pname: + full = f"{prefix}.{pname}" if prefix else pname + if not any( + full == k or full.startswith(k + ".") for k in device_map + ): + device_map[full] = param.device _assign(model, "") + if "" in device_map and len(device_map) > 1: + device_map.pop("") return device_map From a38ceafc888482a2ac3efad7fcb45a15266bc665 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 21 Apr 2026 00:34:27 +0000 Subject: [PATCH 18/21] Add review tests --- ...infer_buffer_only_multi_device_recurses.py | 92 +++++++++++++++++++ ..._llama_seq_class_hook_call_passes_false.py | 70 ++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 test_infer_buffer_only_multi_device_recurses.py create mode 100644 test_llama_seq_class_hook_call_passes_false.py diff --git a/test_infer_buffer_only_multi_device_recurses.py b/test_infer_buffer_only_multi_device_recurses.py new file mode 100644 index 0000000000..773efb51b6 --- /dev/null +++ b/test_infer_buffer_only_multi_device_recurses.py @@ -0,0 +1,92 @@ +import ast +from pathlib import Path +import logging +import warnings as _warnings +import torch + + +def _find_vision(): + for p in [ + Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", + Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", + ]: + if p.exists(): + return p + raise FileNotFoundError("vision.py not found") + + +def _load_fns(): + tree = ast.parse(_find_vision().read_text()) + ns = {"torch": torch, "warnings": _warnings, "logger": logging.getLogger("test")} + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name in { + "_infer_device_map_from_loaded_model", + "_attach_bnb_multidevice_hooks", + }: + exec( + compile( + ast.Module(body = [node], type_ignores = []), + str(_find_vision()), + "exec", + ), + ns, + ) + return ns["_infer_device_map_from_loaded_model"], ns[ + "_attach_bnb_multidevice_hooks" + ] + + +class _P: + def __init__(self, dev): + self.device = torch.device(dev) if isinstance(dev, str) else dev + + +class _BufMod: + def __init__(self, buffers = None, children = None): + self._b = list(buffers or []) + self._c = list(children or []) + self.hf_device_map = None + + def named_parameters(self, recurse = True, remove_duplicate = False): + if False: + yield + + def parameters(self, recurse = True): + if False: + yield + + def named_buffers(self, recurse = True): + for n, d in self._b: + yield n, _P(d) + if recurse: + for cn, cm in self._c: + for bn, bb in cm.named_buffers(recurse = True): + yield f"{cn}.{bn}", bb + + def named_children(self): + yield from self._c + + +def test_buffer_only_subtree_with_multi_device_recurses(): + """Param-less subtree whose buffers span multiple devices must NOT collapse + to the first buffer's device. It must recurse into children so each child + gets its own map entry keyed by its own prefix.""" + infer, _ = _load_fns() + a = _BufMod(buffers = [("cache", "cuda:0")]) + b = _BufMod(buffers = [("cache", "cuda:1")]) + root = _BufMod(children = [("a", a), ("b", b)]) + dm = infer(root) + assert dm.get("a") == torch.device("cuda", 0), dm + assert dm.get("b") == torch.device("cuda", 1), dm + assert "" not in dm, dm + + +def test_buffer_only_subtree_with_single_device_collapses(): + """Param-less subtree with buffers all on one device still collapses to a + single-entry prefix; no accidental recursion when not needed.""" + infer, _ = _load_fns() + a = _BufMod(buffers = [("cache", "cuda:1")]) + b = _BufMod(buffers = [("cache", "cuda:1")]) + root = _BufMod(children = [("a", a), ("b", b)]) + dm = infer(root) + assert dm == {"": torch.device("cuda", 1)}, dm diff --git a/test_llama_seq_class_hook_call_passes_false.py b/test_llama_seq_class_hook_call_passes_false.py new file mode 100644 index 0000000000..a4252b46b9 --- /dev/null +++ b/test_llama_seq_class_hook_call_passes_false.py @@ -0,0 +1,70 @@ +import ast +from pathlib import Path + + +def _find_llama(): + for p in [ + Path(__file__).resolve().parent / "unsloth" / "models" / "llama.py", + Path(__file__).resolve().parents[1] / "unsloth" / "models" / "llama.py", + ]: + if p.exists(): + return p + raise FileNotFoundError("llama.py not found") + + +def _collect_hook_calls_under(node): + """Yield every Call node invoking _attach_bnb_multidevice_hooks within the + AST subtree rooted at `node`.""" + for sub in ast.walk(node): + if ( + isinstance(sub, ast.Call) + and getattr(sub.func, "id", None) == "_attach_bnb_multidevice_hooks" + ): + yield sub + + +def _kwarg_literal(call, name): + for kw in call.keywords: + if kw.arg == name and isinstance(kw.value, ast.Constant): + return kw.value.value + if kw.arg == name: + return kw.value + return None + + +def test_seq_class_branch_passes_fast_inference_false(): + """The AutoModelForSequenceClassification branch (gated by + `if num_labels is not None:`) must pass fast_inference=False to + _attach_bnb_multidevice_hooks. That branch never reaches vLLM, so + forwarding a truthy fast_inference would short-circuit hook install + and regress multi-GPU bnb seq-class inference.""" + tree = ast.parse(_find_llama().read_text()) + + hook_calls = [] + for if_node in ast.walk(tree): + if not isinstance(if_node, ast.If): + continue + test = if_node.test + is_num_labels_not_none = ( + isinstance(test, ast.Compare) + and isinstance(test.left, ast.Name) + and test.left.id == "num_labels" + and len(test.ops) == 1 + and isinstance(test.ops[0], ast.IsNot) + and len(test.comparators) == 1 + and isinstance(test.comparators[0], ast.Constant) + and test.comparators[0].value is None + ) + if not is_num_labels_not_none: + continue + for body_node in if_node.body: + hook_calls.extend(_collect_hook_calls_under(body_node)) + + assert hook_calls, ( + "No _attach_bnb_multidevice_hooks call found under `if num_labels is not None:`" + ) + for call in hook_calls: + v = _kwarg_literal(call, "fast_inference") + assert v is False, ( + f"seq-class hook call must use fast_inference=False (got {ast.dump(call)})" + ) From 0c0039730447360825776b971a606f4f37c65610 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 21 Apr 2026 00:34:27 +0000 Subject: [PATCH 19/21] Add review tests --- ...tach_logger_error_no_misleading_warning.py | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 test_attach_logger_error_no_misleading_warning.py diff --git a/test_attach_logger_error_no_misleading_warning.py b/test_attach_logger_error_no_misleading_warning.py new file mode 100644 index 0000000000..03f482abe2 --- /dev/null +++ b/test_attach_logger_error_no_misleading_warning.py @@ -0,0 +1,108 @@ +import ast +import warnings as _warnings +from pathlib import Path +import torch + + +def _find_vision(): + for p in [ + Path(__file__).resolve().parent / "unsloth" / "models" / "vision.py", + Path(__file__).resolve().parents[1] / "unsloth" / "models" / "vision.py", + ]: + if p.exists(): + return p + raise FileNotFoundError("vision.py not found") + + +class _RaisingLogger: + def info(self, *a, **kw): + raise RuntimeError("simulated broken logging handler") + + +def _load_fns(logger): + tree = ast.parse(_find_vision().read_text()) + ns = {"torch": torch, "warnings": _warnings, "logger": logger} + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name in { + "_infer_device_map_from_loaded_model", + "_attach_bnb_multidevice_hooks", + }: + exec( + compile( + ast.Module(body = [node], type_ignores = []), + str(_find_vision()), + "exec", + ), + ns, + ) + return ns["_infer_device_map_from_loaded_model"], ns[ + "_attach_bnb_multidevice_hooks" + ] + + +class _P: + def __init__(self, dev): + self.device = torch.device(dev) if isinstance(dev, str) else dev + + +class _FakeMod: + def __init__(self, params = None): + self._p = list(params or []) + self.hf_device_map = None + + def named_parameters(self, recurse = True, remove_duplicate = False): + for n, d in self._p: + yield n, _P(d) + + def parameters(self, recurse = True): + for _, p in self.named_parameters(recurse = recurse): + yield p + + def named_buffers(self, recurse = True): + return iter([]) + + def named_children(self): + return iter([]) + + +def test_successful_dispatch_does_not_emit_misleading_warning_when_logger_raises(monkeypatch): + """When dispatch_model succeeds but the user's logger.info raises + (broken handler / strict test harness), the helper must NOT emit the + 'Could not attach multi-device dispatch hooks automatically' warning, + because the hooks *are* installed.""" + import accelerate + + dispatch_called = {"n": 0} + monkeypatch.setattr( + accelerate, + "dispatch_model", + lambda *a, **kw: dispatch_called.__setitem__("n", dispatch_called["n"] + 1), + ) + _, attach = _load_fns(_RaisingLogger()) + m = _FakeMod(params = [("w", "cuda:1")]) + + with _warnings.catch_warnings(record = True) as caught: + _warnings.simplefilter("always") + try: + attach( + m, + load_in_4bit = True, + load_in_8bit = False, + offload_embedding = False, + fast_inference = False, + ) + except RuntimeError: + # Post-fix: logger error may propagate (hooks are installed; + # surfacing a real logging misconfiguration is acceptable). + pass + + assert dispatch_called["n"] == 1, "dispatch_model must have been called" + misleading = [ + w + for w in caught + if "Could not attach multi-device dispatch hooks" in str(w.message) + ] + assert not misleading, ( + f"Must not emit the 'Could not attach' warning when dispatch " + f"actually succeeded (got {[str(w.message) for w in misleading]})" + ) From e169ff2cd7b536ce9fb03b7ab84be5e38a4a7581 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 21 Apr 2026 00:34:27 +0000 Subject: [PATCH 20/21] Consolidate review tests --- test_infer_buffer_only_multi_device_recurses.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test_infer_buffer_only_multi_device_recurses.py b/test_infer_buffer_only_multi_device_recurses.py index 773efb51b6..e67660c36e 100644 --- a/test_infer_buffer_only_multi_device_recurses.py +++ b/test_infer_buffer_only_multi_device_recurses.py @@ -79,14 +79,3 @@ def test_buffer_only_subtree_with_multi_device_recurses(): assert dm.get("a") == torch.device("cuda", 0), dm assert dm.get("b") == torch.device("cuda", 1), dm assert "" not in dm, dm - - -def test_buffer_only_subtree_with_single_device_collapses(): - """Param-less subtree with buffers all on one device still collapses to a - single-entry prefix; no accidental recursion when not needed.""" - infer, _ = _load_fns() - a = _BufMod(buffers = [("cache", "cuda:1")]) - b = _BufMod(buffers = [("cache", "cuda:1")]) - root = _BufMod(children = [("a", a), ("b", b)]) - dm = infer(root) - assert dm == {"": torch.device("cuda", 1)}, dm From 5564fe1750bf68f68dd908f1a8bcb40f638b7a5e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 00:36:51 +0000 Subject: [PATCH 21/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test_attach_logger_error_no_misleading_warning.py | 4 +++- test_llama_seq_class_hook_call_passes_false.py | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/test_attach_logger_error_no_misleading_warning.py b/test_attach_logger_error_no_misleading_warning.py index 03f482abe2..536dd39dd3 100644 --- a/test_attach_logger_error_no_misleading_warning.py +++ b/test_attach_logger_error_no_misleading_warning.py @@ -65,7 +65,9 @@ class _FakeMod: return iter([]) -def test_successful_dispatch_does_not_emit_misleading_warning_when_logger_raises(monkeypatch): +def test_successful_dispatch_does_not_emit_misleading_warning_when_logger_raises( + monkeypatch, +): """When dispatch_model succeeds but the user's logger.info raises (broken handler / strict test harness), the helper must NOT emit the 'Could not attach multi-device dispatch hooks automatically' warning, diff --git a/test_llama_seq_class_hook_call_passes_false.py b/test_llama_seq_class_hook_call_passes_false.py index a4252b46b9..8fb1438200 100644 --- a/test_llama_seq_class_hook_call_passes_false.py +++ b/test_llama_seq_class_hook_call_passes_false.py @@ -60,11 +60,11 @@ def test_seq_class_branch_passes_fast_inference_false(): for body_node in if_node.body: hook_calls.extend(_collect_hook_calls_under(body_node)) - assert hook_calls, ( - "No _attach_bnb_multidevice_hooks call found under `if num_labels is not None:`" - ) + assert ( + hook_calls + ), "No _attach_bnb_multidevice_hooks call found under `if num_labels is not None:`" for call in hook_calls: v = _kwarg_literal(call, "fast_inference") - assert v is False, ( - f"seq-class hook call must use fast_inference=False (got {ast.dump(call)})" - ) + assert ( + v is False + ), f"seq-class hook call must use fast_inference=False (got {ast.dump(call)})"