Skip to content

Commit 48653ec

Browse files
committed
Merge remote-tracking branch 'origin' into kylesayrs/transform_permutations
2 parents 75b9307 + 98a0cd7 commit 48653ec

File tree

15 files changed

+510
-258
lines changed

15 files changed

+510
-258
lines changed

.github/actions/test/action.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,23 @@ runs:
2222
name: compressed
2323
extra: "[dev,accelerate]"
2424

25+
- name: clean up
26+
run: |
27+
echo "cleaning up disk space..."
28+
find . -type f -name '*.whl' -exec rm -rf {} \;
29+
python -m pip cache purge
30+
sudo rm -rf /usr/local/.ghcup
31+
sudo rm -rf /opt/hostedtoolcache/CodeQL
32+
sudo rm -rf /usr/local/lib/android/sdk/ndk
33+
sudo rm -rf /usr/share/dotnet
34+
sudo rm -rf /opt/ghc
35+
sudo rm -rf /usr/local/share/boost
36+
if [[ "$(cat /etc/issue)" =~ Ubuntu ]]; then
37+
sudo apt-get clean
38+
fi
39+
df -h
40+
shell: bash
41+
2542
- name: test
2643
id: test
2744
run: |

.github/workflows/report.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ jobs:
120120
shell: bash
121121

122122
- name: report to reportportal
123-
uses: neuralmagic/nm-actions/actions/reportportal_submit_execution_results@v1.15.0
123+
uses: neuralmagic/nm-actions/actions/reportportal_submit_execution_results@v1.22.0
124124
with:
125125
droute_username: ${{ secrets.DROUTE_USERNAME }}
126126
droute_password: ${{ secrets.DROUTE_PASSWORD }}

.github/workflows/test.yml

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ on:
2222
whl:
2323
description: "whl to test (variable appears late binding so unusable outside 'download artifact')"
2424
type: string
25-
required: true
25+
run_id:
26+
description: run id of the BUILD job that generated the assets
27+
type: string
2628

2729
# makes workflow manually callable
2830
workflow_dispatch:
@@ -44,9 +46,11 @@ on:
4446
type: string
4547
required: true
4648
whl:
47-
description: "whl to test (variable appears late binding so unusable outside 'download artifact')"
49+
description: "whl to test (provide either whl or run_id)"
50+
type: string
51+
run_id:
52+
description: run id of the BUILD job that generated the assets
4853
type: string
49-
required: true
5054

5155
jobs:
5256

@@ -87,11 +91,33 @@ jobs:
8791

8892
- name: download whl
8993
id: download
94+
if: ${{ inputs.whl != '' }}
9095
uses: actions/download-artifact@v4
9196
with:
9297
name: ${{ inputs.whl }}
9398
path: ${{ inputs.whl }}
9499

100+
# GCP
101+
- name: 'Authenticate to Google Cloud'
102+
id: auth
103+
uses: google-github-actions/auth@v2.1.3
104+
with:
105+
project_id: ${{ secrets.GCP_PROJECT }}
106+
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
107+
service_account: ${{ secrets.GCP_GHA_SA }}
108+
109+
- name: 'Set up Cloud SDK'
110+
uses: 'google-github-actions/setup-gcloud@v2'
111+
with:
112+
version: '>= 473.0.0'
113+
114+
- name: download assets
115+
if: ${{ inputs.run_id != '' }}
116+
uses: neuralmagic/nm-actions/actions/gcp-download-assets@v1.1.0
117+
with:
118+
bucket_source: ${{ secrets.GCP_BUILD_ML_ASSETS2 }}
119+
run_id: ${{ inputs.run_id }}
120+
95121
- name: run tests
96122
id: test
97123
uses: ./.github/actions/test/
@@ -109,20 +135,6 @@ jobs:
109135
whl: ${{ inputs.whl }}
110136
test_status: ${{ steps.test.outputs.status }}
111137

112-
# GCP
113-
- name: 'Authenticate to Google Cloud'
114-
id: auth
115-
uses: google-github-actions/auth@v2.1.3
116-
with:
117-
project_id: ${{ secrets.GCP_PROJECT }}
118-
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
119-
service_account: ${{ secrets.GCP_GHA_SA }}
120-
121-
- name: 'Set up Cloud SDK'
122-
uses: 'google-github-actions/setup-gcloud@v2'
123-
with:
124-
version: '>= 473.0.0'
125-
126138
- name: copy results to GCP
127139
run: |
128140
gcloud storage cp test-results/report.xml ${{ secrets.GCP_BUILD_ML_ASSETS2 }}/${{ github.run_id }}/test-results/report-${{ inputs.test_label }}.xml

.github/workflows/trigger-all.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ jobs:
3232
wf_category: ${{ inputs.wf_category || 'NIGHTLY' }}
3333
gitref: ${{ inputs.gitref || 'main' }}
3434
push_to_pypi: ${{ (github.event.schedule == '30 0 * * *') || inputs.push_to_pypi || false }}
35-
test_configs: '[{"python":"3.11.4","label":"ubuntu-22.04","timeout":"40"},
36-
{"python":"3.10.12","label":"ubuntu-24.04","timeout":"40"},
35+
test_configs: '[{"python":"3.11.4","label":"ubuntu-24.04","timeout":"40"},
36+
{"python":"3.10.12","label":"ubuntu-22.04","timeout":"40"},
3737
{"python":"3.9.17","label":"k8s-h100-solo","timeout":"40"},
3838
{"python":"3.12.6","label":"k8s-a100-duo","timeout":"40"}]'
3939

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,5 +113,6 @@ def _setup_extras() -> Dict:
113113
extras_require=_setup_extras(),
114114
install_requires=_setup_install_requires(),
115115
package_dir={"": "src"},
116+
package_data={"": ["transform/utils/hadamards.safetensors"]},
116117
packages=_setup_packages(),
117118
)

src/compressed_tensors/transform/factory/hadamard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def create_transform(self, module: Module, args: TransformArgs):
6161
return HadamardTransform(weight, perm, args)
6262

6363
def _create_weight(self, size: int, dtype: dtype, device: device) -> Parameter:
64-
data = deterministic_hadamard_matrix(size)
64+
data = deterministic_hadamard_matrix(size, dtype, device)
6565
data = data.to(dtype=dtype, device=device)
6666
return Parameter(data, requires_grad=self.scheme.requires_grad)
6767

src/compressed_tensors/transform/factory/random_hadamard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@ class RandomHadamardFactory(HadamardFactory):
2929
"""
3030

3131
def _create_weight(self, size: int, dtype: dtype, device: device) -> Parameter:
32-
data = random_hadamard_matrix(size, self.generator)
32+
data = random_hadamard_matrix(size, dtype, device, self.generator)
3333
data = data.to(dtype=dtype, device=device)
3434
return Parameter(data, requires_grad=self.scheme.requires_grad)

src/compressed_tensors/transform/utils/hadamard.py

Lines changed: 91 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -13,149 +13,148 @@
1313
# limitations under the License.
1414

1515
import math
16-
from typing import Optional, Tuple
16+
from pathlib import Path
17+
from typing import Optional
1718

18-
import numpy
1919
import torch
20+
from safetensors import safe_open
2021

2122

22-
__all__ = ["random_hadamard_matrix", "deterministic_hadamard_matrix"]
23+
REPO_PATH = Path(__file__).parent / "hadamards.safetensors"
2324

24-
# adapted from:
25-
# https://github.com/scipy/scipy/blob/v1.15.2/scipy/linalg/_special_matrices.py
26-
def deterministic_hadamard_matrix(size: int) -> torch.Tensor:
25+
26+
__all__ = ["random_hadamard_matrix", "deterministic_hadamard_matrix", "is_pow2"]
27+
28+
29+
# note that hadamard matrix multiplication can be accelerated using a library such as
30+
# https://github.com/Dao-AILab/fast-hadamard-transform/tree/master
31+
32+
33+
def deterministic_hadamard_matrix(
34+
size: int,
35+
dtype: torch.dtype = torch.bfloat16,
36+
device: torch.device = torch.device("cpu"),
37+
) -> torch.Tensor:
2738
"""
2839
Construct an n-by-n Hadamard matrix, using Sylvester's construction.
2940
`n` must be a power of 2.
3041
42+
Adapated from https://github.com/scipy/scipy/blob/v1.15.2/scipy/linalg/_special_matrices.py # noqa: E501
43+
3144
:param size: order of the matrix, must be a power of 2
45+
:param dtype: data type of matrix
46+
:param device: device to construct matrix on
3247
:return: hadamard matrix of size `size`
3348
"""
3449
if size <= 0:
3550
raise ValueError("Cannot construct deterministic hadamard of size <= 0")
3651

37-
log2 = int(math.log(size, 2))
52+
log2 = int(math.log2(size))
3853
if size != 2**log2:
3954
raise ValueError("Cannot construct deterministic hadamard of size != 2^n")
4055

41-
H = numpy.array([[1]], dtype=int)
56+
H = torch.tensor([[1]], dtype=dtype, device=device)
4257

4358
# Sylvester's construction
44-
for i in range(0, log2):
45-
H = numpy.vstack((numpy.hstack((H, H)), numpy.hstack((H, -H))))
46-
47-
return torch.from_numpy(H / math.sqrt(size))
59+
for _ in range(log2):
60+
H = torch.vstack((torch.hstack((H, H)), torch.hstack((H, -H))))
4861

49-
50-
# adapted from:
51-
# https://github.com/facebookresearch/SpinQuant/blob/main/utils/hadamard_utils.py
52-
53-
# TODO: the following library exists for online rotations and should be considered
54-
# in the future:
55-
# https://github.com/Dao-AILab/fast-hadamard-transform/tree/master
62+
return H / math.sqrt(size)
5663

5764

5865
def random_hadamard_matrix(
59-
size: int, gen: Optional[torch.Generator] = None
66+
size: int,
67+
dtype: torch.dtype = torch.bfloat16,
68+
device: torch.device = torch.device("cpu"),
69+
gen: Optional[torch.Generator] = None,
6070
) -> torch.Tensor:
6171
"""
62-
Produces a randomly generated Hadamard matrix.
63-
See https://cornell-relaxml.github.io/quip-sharp/ ,
64-
Section "Randomized Hadamard Transformation"
72+
Produces a randomly generated Hadamard matrix. Differs from
73+
`deterministic_hadamard_matrix` in that this function supports non powers of 2
74+
and randomization using a seeded generator
75+
76+
Adapated from https://github.com/facebookresearch/SpinQuant/blob/main/utils/hadamard_utils.py # noqa: E501
77+
Known matrices were retrieved from N. J. A. Sloane's Library of Hadamard Matrices http://www.neilsloane.com/hadamard/ # noqa: E501
6578
6679
:param size: The dimension of the hamadard matrix
80+
:param dtype: data type of matrix
81+
:param device: device to construct matrix on
6782
:param gen: Optional generator random values
6883
:return: randomly generated hadamard matrix
6984
"""
70-
# Benefits: support other shapes / non powers of 2, support randomization
71-
Q = torch.randint(low=0, high=2, size=(size,), generator=gen, dtype=torch.float64)
85+
Q = torch.randint(low=0, high=2, size=(size,), generator=gen, dtype=dtype) # cpu
86+
Q = Q.to(device=device)
7287
Q = Q * 2 - 1
7388
Q = torch.diag(Q)
7489
return _matmul_hadU(Q) / math.sqrt(size)
7590

7691

77-
def _get_hadK(n: int, transpose: bool = False) -> Tuple[torch.Tensor, int]:
78-
# NOTE: we can easily extend the list of supported shapes/sizes
79-
# by adding to these methods
80-
hadK, K = None, None
81-
if n % 20 == 0:
82-
assert _is_pow2(n // 20)
83-
K = 20
84-
hadK = _get_had20().T if transpose else _get_had20()
85-
elif n % 12 == 0:
86-
assert _is_pow2(n // 12)
87-
K = 12
88-
hadK = _get_had12().T if transpose else _get_had12()
89-
else:
90-
assert _is_pow2(n)
91-
K = 1
92+
def is_pow2(n: int) -> bool:
93+
"""
94+
Check if a number is a power of 2
9295
93-
return hadK, K
96+
:param n: number to check
97+
:return: True iff `n` is a power of 2
98+
"""
99+
return n > 0 and (n & (n - 1) == 0)
100+
101+
102+
def _fetch_hadamard_divisor(
103+
n: int,
104+
dtype: torch.dtype,
105+
device: torch.device = torch.device("cpu"),
106+
file_path: str = REPO_PATH,
107+
) -> Optional[torch.Tensor]:
108+
"""
109+
Fetch a known hadamard matrix from the given file path. The returned matrix will
110+
be of of size `k` such that `n / k` is a power of two. Return None if no such
111+
matrix exists.
94112
113+
Note: This function reopens the safetensors file every time it is called.
114+
This is technically inefficient, but a very small runtime cost and simpler
115+
than forcing callers to manage the file open context
116+
117+
:param n: size of known hadamard matrix
118+
:return: a known hadamard matrix of size `n` if one exists, else None
119+
"""
120+
with safe_open(file_path, framework="pt", device=str(device)) as file:
121+
divisors = sorted((int(key) for key in file.keys()), reverse=True)
122+
for divisor in divisors:
123+
if n % divisor == 0 and is_pow2(n // divisor):
124+
return file.get_tensor(str(divisor)).to(dtype=dtype)
125+
126+
return None
127+
128+
129+
def _matmul_hadU(X: torch.Tensor) -> torch.Tensor:
130+
size = X.size(0)
131+
dtype = X.dtype
132+
device = X.device
95133

96-
def _matmul_hadU(X, transpose=False) -> torch.Tensor:
97-
n = X.shape[-1]
98134
# Check if we have the determined hadamard matrix
99-
hadK, K = _get_hadK(n, transpose)
135+
hadK = _fetch_hadamard_divisor(size, dtype, device=device)
136+
if hadK is None:
137+
raise ValueError(f"Cannot construct random hadamard matrix of size {size}")
138+
K = hadK.size(0)
139+
100140
# Reshape diag matrix with randomized -1/+1
101-
input = X.clone().view(-1, n, 1)
141+
input = X.clone().view(-1, size, 1)
102142
output = input.clone()
103-
104-
# for cases when hadK is not predetermined, determine hadamard matrix
105143
while input.shape[1] > K:
106144
input = input.view(input.shape[0], input.shape[1] // 2, 2, input.shape[2])
107145
output = output.view(input.shape)
108146
output[:, :, 0, :] = input[:, :, 0, :] + input[:, :, 1, :]
109147
output[:, :, 1, :] = input[:, :, 0, :] - input[:, :, 1, :]
110148
output = output.view(input.shape[0], input.shape[1], -1)
111149
(input, output) = (output, input)
150+
assert input.shape[1] == K
112151
del output
113152

114-
# K == 1 when hadK is None; this happens when the size dim (n)
115-
# is not comaptible with any of the maintained hadamard matrices
116-
117-
if K > 1:
118-
# Do not explicitly repeat - OOM
119-
# input = torch.bmm(
120-
# hadK.repeat(len(input), 1, 1).to(input.device).to(input.dtype), input)
121-
# Use bcast instead
122-
123-
# for cases when hadK is pre-determined
124-
input = hadK.view(1, K, K).to(input) @ input
153+
# Do not explicitly repeat - OOM
154+
# input = torch.bmm(
155+
# hadK.repeat(len(input), 1, 1).to(input.device).to(input.dtype), input)
156+
# Use bcast instead
157+
input = hadK.view(1, K, K).to(input) @ input
125158

126159
# normalize
127160
return input.view(X.shape)
128-
129-
130-
def _is_pow2(n: int) -> bool:
131-
return (n & (n - 1) == 0) and (n > 0)
132-
133-
134-
def _reshape_bits(packed_bits: numpy.ndarray, original_size: int) -> numpy.ndarray:
135-
had_unpacked = numpy.unpackbits(packed_bits)
136-
had_unpacked = [1 if x == 1 else -1 for x in had_unpacked]
137-
had_unpacked = numpy.array(had_unpacked).reshape((original_size, original_size))
138-
return had_unpacked
139-
140-
141-
# http://www.neilsloane.com/hadamard/index.html
142-
def _get_had12() -> torch.Tensor:
143-
# fmt: off
144-
had_12 = numpy.array([128, 13, 29, 232, 235, 71, 218,
145-
62, 209, 246, 139, 180, 157, 168, 237, 199, 106, 59], dtype=numpy.uint8)
146-
# fmt: on
147-
# TODO: just unpack during apply
148-
had_12_unpacked = _reshape_bits(had_12, original_size=12)
149-
return torch.tensor(had_12_unpacked)
150-
151-
152-
def _get_had20() -> torch.Tensor:
153-
# fmt: off
154-
had_20 = numpy.array([128, 0, 13, 133, 121, 236, 43, 203, 97, 94, 155, 10, 252,
155-
216, 87, 230, 194, 191, 54, 21, 249, 176, 171, 205, 133, 222, 108, 42, 243,
156-
97, 215, 155, 10, 188, 216, 149, 230, 200, 175, 54, 133, 121, 188, 43,
157-
205, 225, 94, 107, 10, 243], dtype=numpy.uint8)
158-
# fmt: on
159-
# TODO: just unpack during apply
160-
had_20_unpacked = _reshape_bits(had_20, original_size=20)
161-
return torch.tensor(had_20_unpacked)
Binary file not shown.

0 commit comments

Comments
 (0)