File size: 27,321 Bytes
855e1f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
import os
import time
from fnmatch import fnmatch
from typing import List, Optional, Union, TYPE_CHECKING
import torch

from optimum.quanto.quantize import _quantize_submodule
from optimum.quanto.tensor import Optimizer, qtype, qtypes
from torchao.quantization.quant_api import (
    quantize_ as torchao_quantize_,
    Float8WeightOnlyConfig,
    Int8WeightOnlyConfig
)
from optimum.quanto import freeze
from tqdm import tqdm
from safetensors.torch import load_file
from huggingface_hub import hf_hub_download

from toolkit.print import print_acc
from toolkit.util.ostris_quant import (
    OstrisLinear,
    OstrisQuantizer,
    convert_linear_to_ostris,
    get_ostris_quantizer,
)
import os

if TYPE_CHECKING:
    from toolkit.models.base_model import BaseModel

# the quantize function in quanto had a bug where it was using exclude instead of include

Q_MODULES = [
    "QLinear",
    "QConv2d",
    "QEmbedding",
    "QBatchNorm2d",
    "QLayerNorm",
    "QConvTranspose2d",
    "QEmbeddingBag",
    "OstrisLinear",
]

torchao_qtypes = {
    # "int4": Int4WeightOnlyConfig(),
    # uint2..uint8 are handled by the UIntXQuantizer ostris backend
    # (toolkit/util/uintx_quant.py), a bit-exact reproduction of torchao 0.10.0's
    # UIntXWeightOnlyConfig, so ARAs stay byte-identical after torchao upgrades
    "int8": Int8WeightOnlyConfig(),
    "float8": Float8WeightOnlyConfig(),
}


class aotype:
    def __init__(self, name: str):
        self.name = name
        self.config = torchao_qtypes[name]


class ostristype:
    # custom quantization backend (see toolkit/util/ostris_quant.py), e.g. orbit2/3/4
    def __init__(self, name: str, quantizer: OstrisQuantizer):
        self.name = name
        self.quantizer = quantizer


def get_qtype(qtype: Union[str, qtype]) -> qtype:
    if qtype in torchao_qtypes:
        return aotype(qtype)
    if isinstance(qtype, str):
        ostris_quantizer = get_ostris_quantizer(qtype)
        if ostris_quantizer is not None:
            return ostristype(qtype, ostris_quantizer)
        return qtypes[qtype]
    else:
        return qtype


def is_quantized_tensor(t) -> bool:
    # torchao stores quantized weights as tensor subclasses (e.g. AffineQuantizedTensor) under torchao.*
    # that still report as nn.Parameter and expose .dequantize(). (quanto is handled separately.)
    # _is_ostris_weight tags two OstrisLinear tensors: the .weight property's eager tensor
    # (already dequantized; .dequantize() is a no-op) so the merge paths route through
    # requantize_module_weight, and the lazy OstrisLazyWeight emitted by state_dict()
    # (holds no data; .dequantize() materializes) so save loops dequantize it per key.
    if getattr(t, '_is_ostris_weight', False):
        return True
    return 'torchao' in type(t).__module__ and hasattr(t, 'dequantize')


def dequantize_if_quantized(t):
    return t.dequantize() if is_quantized_tensor(t) else t


def get_torchao_config(qtype):
    # returns the requantization config for a given qtype string (a torchao config, or the
    # ostristype for custom backends), or None if the qtype supports neither
    if qtype is None:
        return None
    try:
        q = get_qtype(qtype)
    except Exception:
        return None
    if isinstance(q, aotype):
        return q.config
    if isinstance(q, ostristype):
        return q
    return None


def requantize_module_weight(module, fp_weight, orig_dtype, config) -> None:
    """Write a full precision weight back into module.weight, re-quantizing in place if a
    requantization config is provided so the module stays quantized (used by the continuous
    merge/reset method). If config is None the weight is left in full precision."""
    if isinstance(module, OstrisLinear):
        # the module's backend reuses its existing quantization state; config is not needed
        module.requantize_(fp_weight)
        return
    if isinstance(config, ostristype):
        # custom backend config but the module was never converted (e.g. skipped at
        # quantize time); leave it in full precision
        config = None
    module.weight = torch.nn.Parameter(fp_weight.to(orig_dtype), requires_grad=False)
    if config is not None:
        torchao_quantize_(module, config)


def _wrap_qlinear_ndim(qlinear: torch.nn.Module) -> None:
    """quanto QLinear forward that tolerates >3D activations by flattening
    the leading dims for the mm and restoring them after (no-op otherwise)."""
    orig_forward = qlinear.forward

    def forward(x):
        if x.ndim > 3:
            lead = x.shape[:-1]
            out = orig_forward(x.reshape(-1, x.shape[-1]))
            return out.reshape(*lead, out.shape[-1])
        return orig_forward(x)

    qlinear.forward = forward
    qlinear._aitk_ndim_wrapped = True


def quantize(
    model: torch.nn.Module,
    weights: Optional[Union[str, qtype, aotype]] = None,
    activations: Optional[Union[str, qtype]] = None,
    optimizer: Optional[Optimizer] = None,
    include: Optional[Union[str, List[str]]] = None,
    exclude: Optional[Union[str, List[str]]] = None,
    quantize_device: Optional[torch.device] = None,
    keep_on_quantize_device: bool = False,
):
    """Quantize the specified model submodules

    Recursively quantize the submodules of the specified parent model.

    Only modules that have quantized counterparts will be quantized.

    If include patterns are specified, the submodule name must match one of them.

    If exclude patterns are specified, the submodule must not match one of them.

    Include or exclude patterns are Unix shell-style wildcards which are NOT regular expressions. See
    https://docs.python.org/3/library/fnmatch.html for more details.

    Note: quantization happens in-place and modifies the original model and its descendants.

    Args:
        model (`torch.nn.Module`): the model whose submodules will be quantized.
        weights (`Optional[Union[str, qtype]]`): the qtype for weights quantization.
        activations (`Optional[Union[str, qtype]]`): the qtype for activations quantization.
        include (`Optional[Union[str, List[str]]]`):
            Patterns constituting the allowlist. If provided, module names must match at
            least one pattern from the allowlist.
        exclude (`Optional[Union[str, List[str]]]`):
            Patterns constituting the denylist. If provided, module names must not match
            any patterns from the denylist.
        quantize_device (`Optional[torch.device]`):
            If provided, each module is moved to this device to quantize, then moved
            back to the device its weights were on initially. Lets a CPU-resident
            model (low vram) quantize layer-by-layer on the GPU.
        keep_on_quantize_device (`bool`):
            With quantize_device set: leave each layer on the quantize device after
            quantizing instead of moving it back. The gpu transient is then one
            bf16 layer at a time — never the whole unquantized remainder at once —
            for models whose final home is that device.
    """
    if include is not None:
        include = [include] if isinstance(include, str) else include
    if exclude is not None:
        exclude = [exclude] if isinstance(exclude, str) else exclude
    for name, m in model.named_modules():
        if include is not None and not any(
            fnmatch(name, pattern) for pattern in include
        ):
            continue
        if exclude is not None and any(fnmatch(name, pattern) for pattern in exclude):
            continue
        try:
            # check if m is QLinear or QConv2d
            if m.__class__.__name__ in Q_MODULES:
                # OstrisLinear may still be RE-quantized into a different
                # ostris qtype (same qtype is a per-layer no-op); every other
                # already-quantized module type is always left alone, which
                # also keeps quanto/torchao from double-quantizing
                # pre-quantized checkpoints
                if not (
                    isinstance(weights, ostristype)
                    and m.__class__.__name__ == "OstrisLinear"
                ):
                    continue
                if getattr(m.ostris_quantizer, "qtype", None) == weights.quantizer.qtype:
                    # guaranteed per-layer no-op: skip before any
                    # quantize_device round-trip
                    continue
            if isinstance(weights, ostristype) and not isinstance(m, torch.nn.Linear):
                # ostris backends only quantize nn.Linear; don't ferry norms/
                # embeddings across the bus for nothing when quantize_device
                # is set (containers fall through so children are visited)
                if quantize_device is not None and next(m.children(), None) is None:
                    continue
            if (
                isinstance(weights, aotype)
                and not isinstance(m, torch.nn.Linear)
                and (
                    quantize_device is not None
                    or include is not None
                    or exclude is not None
                )
            ):
                # torchao only quantizes nn.Linear; when a device round-trip or
                # include/exclude filtering is in play, skip containers so each
                # linear is handled individually (a container-level torchao call
                # would quantize excluded children too)
                continue
            orig_device = None
            if quantize_device is not None and next(m.children(), None) is None:
                # OstrisLinear layers being re-quantized hold buffers, not params
                param = next(m.parameters(recurse=False), None)
                if param is None:
                    param = next(m.buffers(recurse=False), None)
                if param is not None:
                    orig_device = param.device
                    m.to(quantize_device)
            try:
                if isinstance(weights, ostristype):
                    if isinstance(m, torch.nn.Linear):
                        convert_linear_to_ostris(m, weights.quantizer)
                elif isinstance(weights, aotype):
                    torchao_quantize_(m, weights.config)
                else:
                    _quantize_submodule(
                        model,
                        name,
                        m,
                        weights=weights,
                        activations=activations,
                        optimizer=optimizer,
                    )
                    # quanto's qbytes_mm only takes 2D/3D activations; video
                    # patch embeds feed their linears >3D tensors (convrot/
                    # torchao reshape internally). Flatten around the QLinear.
                    replaced = model.get_submodule(name)
                    if replaced.__class__.__name__ == "QLinear" and not getattr(
                        replaced, "_aitk_ndim_wrapped", False
                    ):
                        _wrap_qlinear_ndim(replaced)
            finally:
                if orig_device is not None and not keep_on_quantize_device:
                    # quanto replaces the module in its parent, so re-fetch by name
                    model.get_submodule(name).to(orig_device)
        except Exception as e:
            print(f"Failed to quantize {name}: {e}")
            # raise e


def _has_quantizable_linear(module: torch.nn.Module, weights, exclude=None) -> bool:
    """Whether quantizing ``module`` with ``weights`` would change anything.

    False when every non-excluded linear is already quantized with the same
    ostris qtype (or cannot be quantized at all) — the pre-quantized-checkpoint
    case, where the whole block can be skipped without the device round-trip.
    ``exclude`` patterns are matched against module names relative to
    ``module`` (use leading wildcards for patterns aimed at inner layers)."""
    if not isinstance(weights, ostristype):
        return True
    for name, m in module.named_modules():
        if not isinstance(m, torch.nn.Linear):
            continue
        if exclude is not None and any(fnmatch(name, pattern) for pattern in exclude):
            continue
        if isinstance(m, OstrisLinear):
            if getattr(
                m.ostris_quantizer, "qtype", None
            ) != weights.quantizer.qtype and weights.quantizer.can_quantize(m):
                return True
            continue
        if m.__class__.__name__ in Q_MODULES:
            continue
        if weights.quantizer.can_quantize(m):
            return True
    return False


@torch.no_grad()
def dequantize_ostris_to_linear(module: torch.nn.Module) -> int:
    """Replace every OstrisLinear with a plain nn.Linear holding the full-
    precision weight (activation-side transforms folded), in place, layer by
    layer — the full-precision transient never exceeds one layer. Used when a
    pre-quantized checkpoint is loaded but a DIFFERENT quantization (or none,
    e.g. full finetuning) was requested. Returns the number of layers
    restored."""
    replaced = 0
    for parent in module.modules():
        for child_name, child in list(parent.named_children()):
            if not isinstance(child, OstrisLinear):
                continue
            weight = child.ostris_quantizer.dequantize_folded(child).to(
                child.ostris_orig_dtype
            )
            new = torch.nn.Linear(
                child.in_features,
                child.out_features,
                bias=child.bias is not None,
                device="meta",
                dtype=weight.dtype,
            )
            new.weight = torch.nn.Parameter(weight)
            if child.bias is not None:
                new.bias = torch.nn.Parameter(
                    child.bias.data.to(weight.device, weight.dtype)
                )
            setattr(parent, child_name, new)
            replaced += 1
    return replaced


@torch.no_grad()
def quantize_module(
    module: torch.nn.Module,
    qtype: str,
    device=None,
    dtype: torch.dtype = torch.bfloat16,
    block_names: Optional[List[str]] = None,
    exclude: Optional[List[str]] = None,
    quantize_kwargs: Optional[dict] = None,
    status_fn=print_acc,
    keep_on_device: bool = False,
):
    """Module-centric quantization: block-streamed (each repeated block moves
    to ``device`` for the math) with a whole-module pass for the extras. This
    is the core the per-model loaders call; holders' quantize_model wraps it
    with model_config plumbing.

    ``keep_on_device``: when the model's final home IS ``device`` (no layer
    offloading / low_vram), quantized blocks stay there instead of round-
    tripping back to cpu — the weights then cross the bus exactly once
    (mmap/page-cache -> gpu) instead of three times (up, back into a fresh
    host copy, up again at final placement), and the model-sized host RAM
    spike of that intermediate copy never happens. The extras pass runs on
    the gpu too. With it off (offload paths), blocks return to cpu as before
    and the extras quantize layer-by-layer on ``device`` via quantize_device
    rather than burning every cpu core."""
    from toolkit.dequantize import patch_dequantization_on_save

    patch_dequantization_on_save(module)
    quantization_type = get_qtype(qtype)
    exclude = list(exclude or [])
    quantize_kwargs = quantize_kwargs or {}
    keep_on_device = keep_on_device and device is not None

    all_blocks: List[torch.nn.Module] = []
    for name in block_names or []:
        # name may be a dotted path for models that nest their blocks
        block_list = module
        for part in name.split("."):
            block_list = getattr(block_list, part, None)
            if block_list is None:
                break
        if block_list is not None:
            all_blocks += list(block_list)
    if all_blocks:
        status_fn(f" - quantizing {len(all_blocks)} blocks")
    already_quantized = 0
    debug_phases = os.environ.get("AITK_QUANT_DEBUG") == "1"
    t_check = t_h2d = t_quant = t_d2h = 0.0
    for block in tqdm(all_blocks):
        t = time.perf_counter()
        skip = not _has_quantizable_linear(block, quantization_type, exclude)
        t_check += time.perf_counter() - t
        if skip:
            # pre-quantized checkpoint with a matching qtype: nothing in this
            # block would change — skip the dtype cast entirely so the load
            # stays byte-identical (placement still honors keep_on_device)
            already_quantized += 1
            if keep_on_device:
                block.to(device)
            continue
        t = time.perf_counter()
        if device is not None:
            block.to(device, dtype=dtype, non_blocking=True)
        t_h2d += time.perf_counter() - t
        t = time.perf_counter()
        quantize(block, weights=quantization_type, exclude=exclude, **quantize_kwargs)
        freeze(block)
        t_quant += time.perf_counter() - t
        # NOT non_blocking: an async D2H allocates the cpu destination in pinned
        # memory, which the caching host allocator keeps forever — that silently
        # retained a model-sized chunk of host ram
        t = time.perf_counter()
        if device is not None and not keep_on_device:
            block.to("cpu")
        t_d2h += time.perf_counter() - t
    if debug_phases and all_blocks:
        status_fn(
            f" - [debug] check {t_check:.1f}s h2d {t_h2d:.1f}s "
            f"quant {t_quant:.1f}s d2h {t_d2h:.1f}s"
        )
    if already_quantized:
        status_fn(
            f" - {already_quantized} blocks already quantized with a matching qtype; left untouched"
        )

    status_fn(" - quantizing extras")
    if keep_on_device:
        # blocks already live on the gpu; quantize each remaining layer there
        # one at a time and leave it — the transient is one bf16 layer, never
        # the whole unquantized remainder at once (that transient bump could
        # exceed the model's final footprint by GBs on big-extras models)
        quantize(
            module,
            weights=quantization_type,
            exclude=exclude,
            quantize_device=device,
            keep_on_quantize_device=True,
            **quantize_kwargs,
        )
        # non-quantized leftovers (norms, embeddings, buffers) follow — final
        # residency, not a transient
        module.to(device)
    else:
        # cpu-resident model: quantize each extra layer with a gpu round-trip
        quantize(
            module,
            weights=quantization_type,
            exclude=exclude,
            quantize_device=device,
            **quantize_kwargs,
        )
    freeze(module)
    return module


def quantize_model(
    base_model: "BaseModel",
    model_to_quantize: torch.nn.Module,
):
    from toolkit.dequantize import patch_dequantization_on_save

    if not hasattr(base_model, "get_transformer_block_names"):
        raise ValueError(
            "The model to quantize must have a method `get_transformer_block_names`."
        )

    # patch the state dict method
    patch_dequantization_on_save(model_to_quantize)

    # sensitive modules to keep in full precision (fnmatch patterns)
    exclude_modules = base_model.get_quantization_exclude_modules() or []

    mc = base_model.model_config
    device = base_model.device_torch
    keep_on_device = (
        not mc.low_vram
        and not (mc.layer_offloading and mc.layer_offloading_transformer_percent > 0)
        and torch.device(device).type != "cpu"
    )

    if mc.accuracy_recovery_adapter is not None:
        attach_ara_and_quantize(
            base_model,
            model_to_quantize,
            ara_path=mc.accuracy_recovery_adapter,
            exclude=exclude_modules,
            device=device,
            keep_on_device=keep_on_device,
        )
    else:
        quantize_module(
            model_to_quantize,
            mc.qtype,
            device=device,
            dtype=base_model.torch_dtype,
            block_names=base_model.get_transformer_block_names(),
            exclude=exclude_modules,
            quantize_kwargs=mc.quantize_kwargs,
            status_fn=base_model.print_and_status_update,
            keep_on_device=keep_on_device,
        )


@torch.no_grad()
def attach_ara_and_quantize(
    base_model: "BaseModel",
    model_to_quantize: torch.nn.Module,
    ara_path: str,
    exclude: Optional[List[str]] = None,
    device=None,
    keep_on_device: bool = False,
):
    """Load an accuracy recovery adapter as a live network on the module and
    quantize around it (adapter-hijacked linears at the configured qtype,
    everything else uint8). The network lands on
    base_model.accuracy_recovery_adapter.

    ``device``: quantize each hijacked linear there (gpu kernels) instead of
    wherever it happens to live (historically the cpu — slow). With
    ``keep_on_device`` the quantized linears stay there (final home is that
    gpu); otherwise each returns to the device it came from."""
    from toolkit.config_modules import NetworkConfig
    from toolkit.lora_special import LoRASpecialNetwork

    exclude_modules = list(exclude or [])
    load_lora_path = ara_path

    if not os.path.exists(load_lora_path):
        # not local file, grab from the hub

        path_split = load_lora_path.split("/")
        if len(path_split) > 3:
            raise ValueError(
                "The accuracy recovery adapter path must be a local path or for a hf repo, 'username/repo_name/filename.safetensors'."
            )
        repo_id = f"{path_split[0]}/{path_split[1]}"
        print_acc(f"Grabbing lora from the hub: {load_lora_path}")
        new_lora_path = hf_hub_download(
            repo_id,
            filename=path_split[-1],
        )
        # replace the path
        load_lora_path = new_lora_path

    # build the lora config based on the lora weights
    lora_state_dict = load_file(load_lora_path)
        
    if hasattr(base_model, "convert_lora_weights_before_load"):
        lora_state_dict = base_model.convert_lora_weights_before_load(lora_state_dict)
        
    network_config = {
        "type": "lora",
        "network_kwargs": {"only_if_contains": []},
        "transformer_only": False,
    }
    first_key = list(lora_state_dict.keys())[0]
    first_weight = lora_state_dict[first_key]
    # if it starts with lycoris and includes lokr
    if first_key.startswith("lycoris") and any(
        "lokr" in key for key in lora_state_dict.keys()
    ):
        network_config["type"] = "lokr"
        
    network_kwargs = {}

    # find firse loraA weight
    if network_config["type"] == "lora":
        linear_dim = None
        for key, value in lora_state_dict.items():
            if "lora_A" in key:
                linear_dim = int(value.shape[0])
                break
        linear_alpha = linear_dim
        network_config["linear"] = linear_dim
        network_config["linear_alpha"] = linear_alpha

        # we build the keys to match every key
        only_if_contains = []
        for key in lora_state_dict.keys():
            contains_key = key.split(".lora_")[0]
            if contains_key not in only_if_contains:
                only_if_contains.append(contains_key)

        network_kwargs["only_if_contains"] = only_if_contains
    elif network_config["type"] == "lokr":
        # find the factor
        largest_factor = 0
        for key, value in lora_state_dict.items():
            if "lokr_w1" in key:
                factor = int(value.shape[0])
                if factor > largest_factor:
                    largest_factor = factor
        network_config["lokr_full_rank"] = True
        network_config["lokr_factor"] = largest_factor

        only_if_contains = []
        for key in lora_state_dict.keys():
            if "lokr_w1" in key:
                contains_key = key.split(".lokr_w1")[0]
                contains_key = contains_key.replace("lycoris_", "")
                if contains_key not in only_if_contains:
                    only_if_contains.append(contains_key)
        network_kwargs["only_if_contains"] = only_if_contains
        
    if hasattr(base_model, 'target_lora_modules'):
        network_kwargs['target_lin_modules'] = base_model.target_lora_modules

    # todo auto grab these
    # get dim and scale
    network_config = NetworkConfig(**network_config)

    network = LoRASpecialNetwork(
        text_encoder=None,
        unet=model_to_quantize,
        lora_dim=network_config.linear,
        multiplier=1.0,
        alpha=network_config.linear_alpha,
        # conv_lora_dim=self.network_config.conv,
        # conv_alpha=self.network_config.conv_alpha,
        train_unet=True,
        train_text_encoder=False,
        network_config=network_config,
        network_type=network_config.type,
        transformer_only=network_config.transformer_only,
        is_transformer=base_model.is_transformer,
        base_model=base_model,
        is_ara=True,
        **network_kwargs
    )
    network.apply_to(
        None, model_to_quantize, apply_text_encoder=False, apply_unet=True
    )
    network.force_to(base_model.device_torch, dtype=base_model.torch_dtype)
    network._update_torch_multiplier()
    network.load_weights(lora_state_dict)
    network.eval()
    network.is_active = True
    network.can_merge_in = False
    base_model.accuracy_recovery_adapter = network

    # quantize it
    keep_on_device = keep_on_device and device is not None
    lora_exclude_modules = []
    quantization_type = get_qtype(base_model.model_config.qtype)
    for lora_module in tqdm(network.unet_loras, desc="Attaching quantization"):
        # the lora has already hijacked the original module
        orig_module = lora_module.org_module[0]
        orig_device = None
        if device is not None:
            param = next(orig_module.parameters(), None)
            orig_device = param.device if param is not None else None
            orig_module.to(device, dtype=base_model.torch_dtype)
        else:
            orig_module.to(base_model.torch_dtype)
        # make the params not require gradients
        for param in orig_module.parameters():
            param.requires_grad = False
        quantize(orig_module, weights=quantization_type)
        freeze(orig_module)
        module_name = lora_module.lora_name.replace('$$', '.').replace('transformer.', '')
        lora_exclude_modules.append(module_name)
        if not keep_on_device and orig_device is not None:
            orig_module.to(orig_device)
        elif base_model.model_config.low_vram and device is None:
            # legacy behavior when no quantize device was given
            orig_module.to("cpu")
    # quantize additional layers
    print_acc(" - quantizing additional layers")
    quantization_type = get_qtype('uint8')
    quantize(
        model_to_quantize,
        weights=quantization_type,
        exclude=lora_exclude_modules + exclude_modules,
        quantize_device=device,
        keep_on_quantize_device=keep_on_device,
    )
    if keep_on_device:
        # non-quantized leftovers follow — final residency, not a transient
        model_to_quantize.to(device)