From 346f73de52ab1380969959c4ce64127af95a62b0 Mon Sep 17 00:00:00 2001 From: nathanhubens Date: Mon, 6 Jul 2026 15:09:48 +0200 Subject: [PATCH] fix(metrics): correct params & MACs for quantized models Quantized weights live in packed params (_packed_params), not .parameters(), so get_num_parameters returned 0; thop has no quantized-op handlers so MACs read ~0. But quantization keeps the SAME param & MAC counts (int8 vs fp32) -- only bytes and op-cost drop. Now correct: - get_num_parameters: when _is_quantized, add the numel of quantized modules' callable .weight()/.bias() accessors -> params(quant) == params(fp32). - compute_compute: route quantized models through a forward-hook MAC counter (handles quantized + fp Conv2d/Linear); the thop path is unchanged for fp models. Measured: params 0 -> 5220 (= fp32); MACs 0.008 -> 1.29M (0.99x fp32); disk still drops (int8). Protects the before/after report from fake 'params/MACs -> 0 (-100%)' rows on quantized outputs. nbdev-test green. --- fasterbench/_modidx.py | 4 +++ fasterbench/compute.py | 54 ++++++++++++++++++++++++++++++++ fasterbench/size.py | 40 +++++++++++++++++++++--- nbs/metrics/compute.ipynb | 31 ++++++------------- nbs/metrics/size.ipynb | 65 ++++++--------------------------------- 5 files changed, 114 insertions(+), 80 deletions(-) diff --git a/fasterbench/_modidx.py b/fasterbench/_modidx.py index f7ce56f..171e36b 100644 --- a/fasterbench/_modidx.py +++ b/fasterbench/_modidx.py @@ -35,6 +35,8 @@ 'fasterbench/compute.py'), 'fasterbench.compute.ComputeMetrics.macs_available': ( 'metrics/compute.html#computemetrics.macs_available', 'fasterbench/compute.py'), + 'fasterbench.compute._count_macs_hooks': ( 'metrics/compute.html#_count_macs_hooks', + 'fasterbench/compute.py'), 'fasterbench.compute.compute_compute': ( 'metrics/compute.html#compute_compute', 'fasterbench/compute.py')}, 'fasterbench.core': { 'fasterbench.core._bytes_to_mib': ('core/core.html#_bytes_to_mib', 'fasterbench/core.py'), @@ -166,6 +168,8 @@ 'fasterbench/roofline.py')}, 'fasterbench.size': { 'fasterbench.size.SizeMetrics': ('metrics/size.html#sizemetrics', 'fasterbench/size.py'), 'fasterbench.size.SizeMetrics.as_dict': ('metrics/size.html#sizemetrics.as_dict', 'fasterbench/size.py'), + 'fasterbench.size._quantized_param_count': ( 'metrics/size.html#_quantized_param_count', + 'fasterbench/size.py'), 'fasterbench.size.compute_size': ('metrics/size.html#compute_size', 'fasterbench/size.py'), 'fasterbench.size.get_model_size': ('metrics/size.html#get_model_size', 'fasterbench/size.py'), 'fasterbench.size.get_num_parameters': ('metrics/size.html#get_num_parameters', 'fasterbench/size.py')}, diff --git a/fasterbench/compute.py b/fasterbench/compute.py index 777b4bd..70b5e40 100644 --- a/fasterbench/compute.py +++ b/fasterbench/compute.py @@ -3,12 +3,15 @@ # %% ../nbs/metrics/compute.ipynb #0091d170 from __future__ import annotations +import math import warnings from dataclasses import dataclass import torch import torch.nn as nn +from .core import _is_quantized + try: from thop import profile as _thop_profile except ImportError: @@ -39,6 +42,48 @@ def as_dict(self) -> dict[str, float]: } +#| export +def _count_macs_hooks( + model: nn.Module, # model to profile (run on CPU) + sample: torch.Tensor, # input tensor (with batch dimension) +) -> int: + """Count Conv2d/Linear MACs via forward hooks (fp *and* quantized modules). + + thop has no handlers for quantized ops (it reports ~0), so we count manually. + Quantization does not change the number of multiply-accumulates: an int8 conv + does the same MACs as its fp32 twin. Uses module attributes (`out_channels`, + `kernel_size`, `in_features`, ...) which both fp and quantized modules expose, + and reads output spatial dims from the produced tensor. + """ + macs = 0 + handles = [] + + def conv_hook(m, inp, out): + nonlocal macs + out_spatial = math.prod(out.shape[2:]) # out_H * out_W + kernel = math.prod(m.kernel_size) # kH * kW + macs += out.shape[0] * m.out_channels * (m.in_channels // m.groups) * kernel * out_spatial + + def linear_hook(m, inp, out): + nonlocal macs + leading = out.numel() // m.out_features # batch/token dims + macs += leading * m.in_features * m.out_features + + for m in model.modules(): + if hasattr(m, "kernel_size") and hasattr(m, "out_channels") and hasattr(m, "groups"): + handles.append(m.register_forward_hook(conv_hook)) + elif hasattr(m, "in_features") and hasattr(m, "out_features"): + handles.append(m.register_forward_hook(linear_hook)) + + try: + with torch.no_grad(): + model(sample) + finally: + for h in handles: + h.remove() + return macs + + #| export def compute_compute( model: nn.Module, # model to analyze @@ -55,6 +100,15 @@ def compute_compute( macs_m: float | None = None + # Quantized models: thop has no quantized-op handlers (returns ~0). Count the + # Conv2d/Linear MACs manually — quantization preserves the MAC count. + if _is_quantized(model): + try: + macs_m = round(_count_macs_hooks(model, sample) / 1e6, 3) + except Exception as e: + warnings.warn(f"quantized MAC counting failed: {e}") + return ComputeMetrics(macs_m=macs_m) + if _thop_profile is not None: try: mac_raw, _ = _thop_profile(model, inputs=(sample,), verbose=False) diff --git a/fasterbench/size.py b/fasterbench/size.py index b767f25..e596b62 100644 --- a/fasterbench/size.py +++ b/fasterbench/size.py @@ -3,7 +3,7 @@ # %% ../nbs/metrics/size.ipynb #4c37777e from __future__ import annotations -from .core import _bytes_to_mib +from .core import _bytes_to_mib, _is_quantized import torch import io from dataclasses import dataclass, asdict @@ -22,15 +22,47 @@ def get_model_size(model: torch.nn.Module) -> int: # model to measure return buf.getbuffer().nbytes +#| export +def _quantized_param_count(model: torch.nn.Module) -> int: # model with quantized modules + """Sum the numel of packed quantized weights/biases (not in `.parameters()`). + + Quantized `Conv2d`/`Linear` keep their weights in `_packed_params`, exposed via + a *callable* `.weight()`/`.bias()` accessor (unlike a plain `nn.Parameter`, which + is a tensor). Quantization preserves the number of parameters, so counting these + makes a quantized model's param count match its fp32 twin. + """ + total = 0 + for m in model.modules(): + for name in ("weight", "bias"): + accessor = getattr(m, name, None) + if not callable(accessor): # plain nn.Parameter tensors are not callable + continue + try: + t = accessor() + if t is not None: + total += t.numel() + except Exception: + pass + return total + + #| export def get_num_parameters( model: torch.nn.Module, # model to count parameters trainable_only: bool = True, # if True, only count trainable parameters ) -> int: - """Count the number of (optionally trainable) parameters.""" + """Count the number of (optionally trainable) parameters. + + Quantized weights live in packed params outside `.parameters()`, so they are + added separately (regardless of `trainable_only`, as they are never trainable). + """ if trainable_only: - return sum(p.numel() for p in model.parameters() if p.requires_grad) - return sum(p.numel() for p in model.parameters()) + n = sum(p.numel() for p in model.parameters() if p.requires_grad) + else: + n = sum(p.numel() for p in model.parameters()) + if _is_quantized(model): + n += _quantized_param_count(model) + return n #| export diff --git a/nbs/metrics/compute.ipynb b/nbs/metrics/compute.ipynb index 261b566..3df3272 100644 --- a/nbs/metrics/compute.ipynb +++ b/nbs/metrics/compute.ipynb @@ -34,26 +34,7 @@ "id": "0091d170", "metadata": {}, "outputs": [], - "source": [ - "#| export\n", - "from __future__ import annotations\n", - "\n", - "import warnings\n", - "from dataclasses import dataclass\n", - "\n", - "import torch\n", - "import torch.nn as nn\n", - "\n", - "try:\n", - " from thop import profile as _thop_profile\n", - "except ImportError:\n", - " _thop_profile = None\n", - "\n", - "try:\n", - " from torchprofile import profile_macs as _profile_macs\n", - "except ImportError:\n", - " _profile_macs = None" - ] + "source": "#| export\nfrom __future__ import annotations\n\nimport math\nimport warnings\nfrom dataclasses import dataclass\n\nimport torch\nimport torch.nn as nn\n\nfrom fasterbench.core import _is_quantized\n\ntry:\n from thop import profile as _thop_profile\nexcept ImportError:\n _thop_profile = None\n\ntry:\n from torchprofile import profile_macs as _profile_macs\nexcept ImportError:\n _profile_macs = None" }, { "cell_type": "code", @@ -61,7 +42,7 @@ "id": "09401efb", "metadata": {}, "outputs": [], - "source": "#| export\n@dataclass(slots=True)\nclass ComputeMetrics:\n \"\"\"MACs (Multiply-Accumulate operations) in millions.\"\"\"\n macs_m: float | None # None if unavailable\n\n @property\n def macs_available(self) -> bool:\n \"\"\"Check if MACs measurement succeeded.\"\"\"\n return self.macs_m is not None\n\n def as_dict(self) -> dict[str, float]:\n return {\n \"macs_m\": self.macs_m if self.macs_m is not None else float(\"nan\"),\n }\n\n\n#| export\ndef compute_compute(\n model: nn.Module, # model to analyze\n sample: torch.Tensor, # input tensor (with batch dimension)\n) -> ComputeMetrics:\n \"\"\"Compute MACs for a single forward pass.\"\"\"\n try:\n model_device = next(model.parameters()).device\n except StopIteration:\n model_device = torch.device(\"cpu\")\n\n if sample.device != model_device:\n sample = sample.to(model_device)\n\n macs_m: float | None = None\n\n if _thop_profile is not None:\n try:\n mac_raw, _ = _thop_profile(model, inputs=(sample,), verbose=False)\n macs_m = round(mac_raw / 1e6, 3)\n except Exception as e:\n warnings.warn(f\"thop failed: {e}\")\n elif _profile_macs is not None:\n try:\n macs_m = round(_profile_macs(model, sample) / 1e6, 3)\n except Exception as e:\n warnings.warn(f\"torchprofile failed: {e}\")\n else:\n warnings.warn(\"No MAC-counting backend available – skipping MACs\")\n\n return ComputeMetrics(macs_m=macs_m)" + "source": "#| export\n@dataclass(slots=True)\nclass ComputeMetrics:\n \"\"\"MACs (Multiply-Accumulate operations) in millions.\"\"\"\n macs_m: float | None # None if unavailable\n\n @property\n def macs_available(self) -> bool:\n \"\"\"Check if MACs measurement succeeded.\"\"\"\n return self.macs_m is not None\n\n def as_dict(self) -> dict[str, float]:\n return {\n \"macs_m\": self.macs_m if self.macs_m is not None else float(\"nan\"),\n }\n\n\n#| export\ndef _count_macs_hooks(\n model: nn.Module, # model to profile (run on CPU)\n sample: torch.Tensor, # input tensor (with batch dimension)\n) -> int:\n \"\"\"Count Conv2d/Linear MACs via forward hooks (fp *and* quantized modules).\n\n thop has no handlers for quantized ops (it reports ~0), so we count manually.\n Quantization does not change the number of multiply-accumulates: an int8 conv\n does the same MACs as its fp32 twin. Uses module attributes (`out_channels`,\n `kernel_size`, `in_features`, ...) which both fp and quantized modules expose,\n and reads output spatial dims from the produced tensor.\n \"\"\"\n macs = 0\n handles = []\n\n def conv_hook(m, inp, out):\n nonlocal macs\n out_spatial = math.prod(out.shape[2:]) # out_H * out_W\n kernel = math.prod(m.kernel_size) # kH * kW\n macs += out.shape[0] * m.out_channels * (m.in_channels // m.groups) * kernel * out_spatial\n\n def linear_hook(m, inp, out):\n nonlocal macs\n leading = out.numel() // m.out_features # batch/token dims\n macs += leading * m.in_features * m.out_features\n\n for m in model.modules():\n if hasattr(m, \"kernel_size\") and hasattr(m, \"out_channels\") and hasattr(m, \"groups\"):\n handles.append(m.register_forward_hook(conv_hook))\n elif hasattr(m, \"in_features\") and hasattr(m, \"out_features\"):\n handles.append(m.register_forward_hook(linear_hook))\n\n try:\n with torch.no_grad():\n model(sample)\n finally:\n for h in handles:\n h.remove()\n return macs\n\n\n#| export\ndef compute_compute(\n model: nn.Module, # model to analyze\n sample: torch.Tensor, # input tensor (with batch dimension)\n) -> ComputeMetrics:\n \"\"\"Compute MACs for a single forward pass.\"\"\"\n try:\n model_device = next(model.parameters()).device\n except StopIteration:\n model_device = torch.device(\"cpu\")\n\n if sample.device != model_device:\n sample = sample.to(model_device)\n\n macs_m: float | None = None\n\n # Quantized models: thop has no quantized-op handlers (returns ~0). Count the\n # Conv2d/Linear MACs manually — quantization preserves the MAC count.\n if _is_quantized(model):\n try:\n macs_m = round(_count_macs_hooks(model, sample) / 1e6, 3)\n except Exception as e:\n warnings.warn(f\"quantized MAC counting failed: {e}\")\n return ComputeMetrics(macs_m=macs_m)\n\n if _thop_profile is not None:\n try:\n mac_raw, _ = _thop_profile(model, inputs=(sample,), verbose=False)\n macs_m = round(mac_raw / 1e6, 3)\n except Exception as e:\n warnings.warn(f\"thop failed: {e}\")\n elif _profile_macs is not None:\n try:\n macs_m = round(_profile_macs(model, sample) / 1e6, 3)\n except Exception as e:\n warnings.warn(f\"torchprofile failed: {e}\")\n else:\n warnings.warn(\"No MAC-counting backend available – skipping MACs\")\n\n return ComputeMetrics(macs_m=macs_m)" }, { "cell_type": "code", @@ -101,6 +82,14 @@ "#| hide\nfrom fastcore.test import *\n\nimport torch, torch.nn as nn\n_m = nn.Linear(10, 5)\n_x = torch.randn(1, 10)\n_c = compute_compute(_m, _x)\nassert isinstance(_c, ComputeMetrics)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "b7d64a51", + "metadata": {}, + "outputs": [], + "source": "#| hide\n# Quantization preserves the *number* of MACs (int8 conv/linear do the same\n# multiply-accumulates as their fp32 twin) — only the ops get cheaper. But thop\n# has no quantized-op handlers, so it reports ~0. Verify the manual hook counter\n# recovers a MAC count ~equal to the fp32 twin's thop count.\nfrom torch.ao.quantization import get_default_qconfig, QConfigMapping\nfrom torch.ao.quantization.quantize_fx import prepare_fx, convert_fx\nfrom fasterbench.core import _is_quantized\n\nclass _MacNet(nn.Module):\n \"Conv-dominated net (linear/pool MACs negligible), statically quantizable via FX.\"\n def __init__(self):\n super().__init__()\n self.c1, self.c2 = nn.Conv2d(3, 16, 3, padding=1), nn.Conv2d(16, 32, 3, padding=1)\n self.pool, self.flat, self.fc = nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(32, 10)\n def forward(self, x):\n x = torch.relu(self.c1(x)); x = torch.relu(self.c2(x))\n return self.fc(self.flat(self.pool(x)))\n\n_fp = _MacNet().eval()\n_x = torch.randn(1, 3, 16, 16)\n_fp_macs = compute_compute(_fp, _x).macs_m # fp32 baseline (thop path)\nassert _fp_macs is not None and _fp_macs > 0\n\ntorch.backends.quantized.engine = \"x86\"\n_qmap = QConfigMapping().set_global(get_default_qconfig(\"x86\"))\n_prep = prepare_fx(_MacNet().eval(), _qmap, example_inputs=(_x,))\n_prep(_x) # calibrate\n_qm = convert_fx(_prep)\nassert _is_quantized(_qm)\n\n# BEFORE the fix, thop reports ~0 MACs for a fully quantized model.\n_thop_raw, _ = _thop_profile(_qm, inputs=(_x,), verbose=False)\nassert _thop_raw / 1e6 < 0.1 * _fp_macs # thop is effectively blind here\n\n_q = compute_compute(_qm, _x) # AFTER the fix (manual counter)\nassert _q.macs_m is not None and _q.macs_m > 0 # zero is gone\nassert abs(_q.macs_m - _fp_macs) <= 0.05 * _fp_macs # within ~5% of fp32 MACs" + }, { "cell_type": "markdown", "id": "see_also", diff --git a/nbs/metrics/size.ipynb b/nbs/metrics/size.ipynb index b7a1f31..b726a87 100644 --- a/nbs/metrics/size.ipynb +++ b/nbs/metrics/size.ipynb @@ -34,15 +34,7 @@ "id": "4c37777e", "metadata": {}, "outputs": [], - "source": [ - "#| export\n", - "from __future__ import annotations\n", - "\n", - "from fasterbench.core import _bytes_to_mib\n", - "import torch\n", - "import io\n", - "from dataclasses import dataclass, asdict" - ] + "source": "#| export\nfrom __future__ import annotations\n\nfrom fasterbench.core import _bytes_to_mib, _is_quantized\nimport torch\nimport io\nfrom dataclasses import dataclass, asdict" }, { "cell_type": "code", @@ -50,52 +42,7 @@ "id": "daa12120", "metadata": {}, "outputs": [], - "source": [ - "#| export\n", - "def get_model_size(model: torch.nn.Module) -> int: # model to measure\n", - " \"\"\"Return the on-disk size (bytes) of the serialized model.\"\"\"\n", - " buf = io.BytesIO()\n", - " try:\n", - " model.save(buf)\n", - " except Exception:\n", - " torch.save(model.state_dict(), buf)\n", - " return buf.getbuffer().nbytes\n", - "\n", - "\n", - "#| export\n", - "def get_num_parameters(\n", - " model: torch.nn.Module, # model to count parameters\n", - " trainable_only: bool = True, # if True, only count trainable parameters\n", - ") -> int:\n", - " \"\"\"Count the number of (optionally trainable) parameters.\"\"\"\n", - " if trainable_only:\n", - " return sum(p.numel() for p in model.parameters() if p.requires_grad)\n", - " return sum(p.numel() for p in model.parameters())\n", - "\n", - "\n", - "#| export\n", - "@dataclass(slots=True)\n", - "class SizeMetrics:\n", - " \"\"\"Model size metrics: disk size and parameter count.\"\"\"\n", - " disk_bytes: int\n", - " size_mib: float\n", - " num_params: int\n", - "\n", - " def as_dict(self) -> dict[str, float]:\n", - " return asdict(self)\n", - "\n", - "\n", - "#| export\n", - "def compute_size(\n", - " model: torch.nn.Module, # model to measure\n", - " *,\n", - " params_count: int | None = None, # pre-computed parameter count (avoids recount)\n", - ") -> SizeMetrics:\n", - " \"\"\"Compute size metrics for a model.\"\"\"\n", - " disk = get_model_size(model)\n", - " params = params_count if params_count is not None else get_num_parameters(model)\n", - " return SizeMetrics(disk_bytes=disk, size_mib=_bytes_to_mib(disk), num_params=params)" - ] + "source": "#| export\ndef get_model_size(model: torch.nn.Module) -> int: # model to measure\n \"\"\"Return the on-disk size (bytes) of the serialized model.\"\"\"\n buf = io.BytesIO()\n try:\n model.save(buf)\n except Exception:\n torch.save(model.state_dict(), buf)\n return buf.getbuffer().nbytes\n\n\n#| export\ndef _quantized_param_count(model: torch.nn.Module) -> int: # model with quantized modules\n \"\"\"Sum the numel of packed quantized weights/biases (not in `.parameters()`).\n\n Quantized `Conv2d`/`Linear` keep their weights in `_packed_params`, exposed via\n a *callable* `.weight()`/`.bias()` accessor (unlike a plain `nn.Parameter`, which\n is a tensor). Quantization preserves the number of parameters, so counting these\n makes a quantized model's param count match its fp32 twin.\n \"\"\"\n total = 0\n for m in model.modules():\n for name in (\"weight\", \"bias\"):\n accessor = getattr(m, name, None)\n if not callable(accessor): # plain nn.Parameter tensors are not callable\n continue\n try:\n t = accessor()\n if t is not None:\n total += t.numel()\n except Exception:\n pass\n return total\n\n\n#| export\ndef get_num_parameters(\n model: torch.nn.Module, # model to count parameters\n trainable_only: bool = True, # if True, only count trainable parameters\n) -> int:\n \"\"\"Count the number of (optionally trainable) parameters.\n\n Quantized weights live in packed params outside `.parameters()`, so they are\n added separately (regardless of `trainable_only`, as they are never trainable).\n \"\"\"\n if trainable_only:\n n = sum(p.numel() for p in model.parameters() if p.requires_grad)\n else:\n n = sum(p.numel() for p in model.parameters())\n if _is_quantized(model):\n n += _quantized_param_count(model)\n return n\n\n\n#| export\n@dataclass(slots=True)\nclass SizeMetrics:\n \"\"\"Model size metrics: disk size and parameter count.\"\"\"\n disk_bytes: int\n size_mib: float\n num_params: int\n\n def as_dict(self) -> dict[str, float]:\n return asdict(self)\n\n\n#| export\ndef compute_size(\n model: torch.nn.Module, # model to measure\n *,\n params_count: int | None = None, # pre-computed parameter count (avoids recount)\n) -> SizeMetrics:\n \"\"\"Compute size metrics for a model.\"\"\"\n disk = get_model_size(model)\n params = params_count if params_count is not None else get_num_parameters(model)\n return SizeMetrics(disk_bytes=disk, size_mib=_bytes_to_mib(disk), num_params=params)" }, { "cell_type": "code", @@ -147,6 +94,14 @@ "#| hide\nfrom fastcore.test import *\n\nimport torch.nn as nn\n_m = nn.Linear(10, 5)\n_s = compute_size(_m)\nassert isinstance(_s, SizeMetrics)\nassert _s.num_params > 0\ntest_eq(get_num_parameters(_m), 55)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "52ea5e13", + "metadata": {}, + "outputs": [], + "source": "#| hide\n# Quantization preserves the *number* of params (int8 conv/linear have the same\n# weight count as their fp32 twin) — only the bytes shrink. But quantized weights\n# live in packed params outside `.parameters()`, so the naive sum reports 0.\n# Verify: quantized param count is non-zero and ~equal to the fp32 count.\nimport torch.nn as nn\nfrom torch.ao.quantization import get_default_qconfig, QConfigMapping\nfrom torch.ao.quantization.quantize_fx import prepare_fx, convert_fx\nfrom fasterbench.core import _is_quantized\n\nclass _SizeNet(nn.Module):\n \"Small conv+linear net, statically quantizable via FX.\"\n def __init__(self):\n super().__init__()\n self.c1, self.c2 = nn.Conv2d(3, 16, 3, padding=1), nn.Conv2d(16, 32, 3, padding=1)\n self.pool, self.flat, self.fc = nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(32, 10)\n def forward(self, x):\n x = torch.relu(self.c1(x)); x = torch.relu(self.c2(x))\n return self.fc(self.flat(self.pool(x)))\n\n_fp = _SizeNet().eval()\n_x = torch.randn(1, 3, 16, 16)\n_fp_params = get_num_parameters(_fp) # fp32 baseline\n\ntorch.backends.quantized.engine = \"x86\"\n_qmap = QConfigMapping().set_global(get_default_qconfig(\"x86\"))\n_prep = prepare_fx(_SizeNet().eval(), _qmap, example_inputs=(_x,))\n_prep(_x) # calibrate\n_qm = convert_fx(_prep)\nassert _is_quantized(_qm)\n\n# BEFORE the fix, the naive `.parameters()` sum is 0 for a fully quantized model.\ntest_eq(sum(p.numel() for p in _qm.parameters() if p.requires_grad), 0)\n\n_q_params = get_num_parameters(_qm) # AFTER the fix\nassert _q_params > 0 # zero is gone\nassert abs(_q_params - _fp_params) <= 0.05 * _fp_params # within ~5% of fp32\n# trainable_only must NOT hide quantized weights (they are never trainable)\ntest_eq(get_num_parameters(_qm, trainable_only=False), _q_params)" + }, { "cell_type": "markdown", "execution_count": null,