File size: 8,589 Bytes
a62b6c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
# SPDX-License-Identifier: Apache-2.0

"""
Tests for distributing a torch.Tensor using the topology of an existing TTNN tensor.

Follows the same style as test_auto_compose.py, but in the opposite
direction: given a reference distributed tensor, verify that
`from_torch_dist_as(torch_tensor, ref_tensor)` produces a TTNN tensor
with the same topology and data that composes back to the original
torch input.
"""

import pytest
import torch

import ttnn
from models.common.auto_compose import extract_tensor_topology_info, to_torch_auto_compose
from models.common.distribute_as import from_torch_dist_as

# ======================================================================================
# Test Parameters
# ======================================================================================


pytestmark = [
    pytest.mark.parametrize(
        "ttnn_mesh_device",
        [
            (1, 1),  # single device
            (1, 2),  # 1D mesh, 2 devices
            (1, 8),  # 1D mesh, 8 devices
            (2, 4),  # 2D mesh, 8 devices
        ],
        ids=[
            "1x1",
            "1x2",
            "1x8",
            "2x4",
        ],
        indirect=True,
    ),
    pytest.mark.parametrize(
        "layout,dtype",
        [
            (ttnn.ROW_MAJOR_LAYOUT, ttnn.bfloat16),
            (ttnn.TILE_LAYOUT, ttnn.bfloat16),
            (ttnn.TILE_LAYOUT, ttnn.bfloat8_b),  # bfloat8_b only works with TILE_LAYOUT
            (ttnn.TILE_LAYOUT, ttnn.bfloat4_b),  # bfloat4_b only works with TILE_LAYOUT
        ],
        ids=["row_major_bf16", "tile_bf16", "tile_bf8b", "tile_bf4b"],
    ),
    pytest.mark.parametrize(
        "memory_config",
        [ttnn.DRAM_MEMORY_CONFIG, ttnn.L1_MEMORY_CONFIG],
        ids=["dram", "l1"],
    ),
]


# ======================================================================================
# Helpers
# ======================================================================================


def _make_arange_dtype(
    shape: tuple[int, ...], dtype: torch.dtype = torch.bfloat16, min_value: float = 0, max_value: float = 100
) -> torch.Tensor:
    """Create a deterministic tensor with arange data and specified dtype."""
    numel = 1
    for s in shape:
        numel *= s
    # Generate values from min_value to max_value with step of 1
    values = torch.arange(min_value, max_value + 1, dtype=dtype)
    # Randomly sample indices (with replacement) to fill the tensor
    indices = torch.randint(0, len(values), size=(numel,))
    data = values[indices].reshape(shape)
    return data


def _pos_dim(dim: int, rank: int) -> int:
    return dim % rank


def _topology_signature(tt: ttnn.Tensor) -> tuple[tuple[str, tuple[int, ...]], tuple[int, ...]]:
    placements, dist_shape = extract_tensor_topology_info(tt)
    sig_p = []
    for p in placements:
        if isinstance(p, ttnn.PlacementShard):
            sig_p.append(("shard", (p.dim,)))
        else:
            assert isinstance(p, ttnn.PlacementReplicate)
            sig_p.append(("replicate", ()))
    return tuple(sig_p), tuple(dist_shape)


# ======================================================================================
# Tests
# ======================================================================================


@pytest.mark.parametrize("storage", ["host", "device"])
def test_sharded_1d(
    ttnn_mesh_device: ttnn.MeshDevice, layout, dtype, memory_config: ttnn.MemoryConfig, storage: str
) -> None:
    is_host_ref = storage == "host"
    num_devices = ttnn_mesh_device.get_num_devices()

    # Reference topology: host-sharded along dim 0
    shape = (num_devices, 1, 3, 1)
    # get range of values for bfloat4_b quantization which has 4 bits for the mantissa and shared 8-bit exponent
    max_value = 6  # allows for range of value to grow to 7 for new_input
    ref_input = _make_arange_dtype(shape, dtype=torch.float32, min_value=-7, max_value=max_value)
    ref_tt = ttnn.from_torch(
        ref_input,
        device=None if is_host_ref else ttnn_mesh_device,
        dtype=dtype,
        layout=layout,
        memory_config=memory_config,
        mesh_mapper=ttnn.ShardTensorToMesh(ttnn_mesh_device, dim=0),
    )

    # Distribute a different tensor using the same topology
    new_input = ref_input + 1  # change values to avoid accidental equality
    if is_host_ref:
        tt_as = from_torch_dist_as(new_input, ref_tt, device=ttnn_mesh_device)
    else:
        tt_as = from_torch_dist_as(new_input, ref_tt)

    # Verify topology matches reference
    expected_topology = _topology_signature(ref_tt)
    assert _topology_signature(tt_as) == expected_topology
    assert tt_as.dtype == dtype
    assert tt_as.memory_config() == memory_config

    # Verify round-trip via auto-composition matches the source
    if is_host_ref:
        torch_auto = to_torch_auto_compose(tt_as, device=ttnn_mesh_device)
    else:
        torch_auto = to_torch_auto_compose(tt_as)
    assert torch.equal(torch_auto, new_input)


@pytest.mark.parametrize("storage", ["host", "device"])
@pytest.mark.parametrize("dim", [0, 1, 2, -1])
def test_sharded_various_dims(
    ttnn_mesh_device: ttnn.MeshDevice,
    layout,
    dtype,
    memory_config: ttnn.MemoryConfig,
    dim: int,
    storage: str,
) -> None:
    is_host_ref = storage == "host"
    num_devices = ttnn_mesh_device.get_num_devices()

    rank = 4
    axis = _pos_dim(dim, rank)
    shape = [2, 3, 4, 1]
    shape[axis] = num_devices
    # get range of values for bfloat4_b quantization which has 4 bits for the mantissa and shared 8-bit exponent
    max_value = 6  # allows for range of value to grow to 7 for new_input
    ref_input = _make_arange_dtype(shape, dtype=torch.float32, min_value=-7, max_value=max_value)

    ref_tt = ttnn.from_torch(
        ref_input,
        device=None if is_host_ref else ttnn_mesh_device,
        dtype=dtype,
        layout=layout,
        memory_config=memory_config,
        mesh_mapper=ttnn.ShardTensorToMesh(ttnn_mesh_device, dim=dim),
    )

    new_input = ref_input + 1  # change values to avoid accidental equality
    if is_host_ref:
        tt_as = from_torch_dist_as(new_input, ref_tt, device=ttnn_mesh_device)
    else:
        tt_as = from_torch_dist_as(new_input, ref_tt)

    assert _topology_signature(tt_as) == _topology_signature(ref_tt)
    assert tt_as.dtype == dtype
    assert tt_as.memory_config() == memory_config
    if is_host_ref:
        torch_auto = to_torch_auto_compose(tt_as, device=ttnn_mesh_device)
    else:
        torch_auto = to_torch_auto_compose(tt_as)
    assert torch.equal(torch_auto, new_input)


@pytest.mark.parametrize("storage", ["host", "device"])
@pytest.mark.parametrize("dims_pair", [(0, 1), (0, -1), (1, -1)])
def test_sharded_2d(
    ttnn_mesh_device: ttnn.MeshDevice,
    layout,
    dtype,
    memory_config: ttnn.MemoryConfig,
    dims_pair: tuple[int, int],
    storage: str,
) -> None:
    is_host_ref = storage == "host"
    mesh_shape = tuple(ttnn_mesh_device.shape)
    if len(mesh_shape) != 2 and torch.prod(torch.tensor(mesh_shape)).item() <= 1:
        pytest.skip("Requires a 2D mesh with both dims > 1")

    rank = 4
    d0 = _pos_dim(dims_pair[0], rank)
    d1 = _pos_dim(dims_pair[1], rank)
    assert d0 != d1

    shape = [2, 3, 4, 5]
    shape[d0] = mesh_shape[0]
    shape[d1] = mesh_shape[1]
    # get range of values for bfloat4_b quantization which has 4 bits for the mantissa and shared 8-bit exponent
    max_value = 6  # allows for range of value to grow to 7 for new_input
    ref_input = _make_arange_dtype(shape, dtype=torch.float32, min_value=-7, max_value=max_value)

    mapper = ttnn.ShardTensor2dMesh(ttnn_mesh_device, mesh_shape=mesh_shape, dims=(dims_pair[0], dims_pair[1]))
    ref_tt = ttnn.from_torch(
        ref_input,
        device=None if is_host_ref else ttnn_mesh_device,
        dtype=dtype,
        layout=layout,
        memory_config=memory_config,
        mesh_mapper=mapper,
    )

    new_input = ref_input + 1  # change values to avoid accidental equality
    if is_host_ref:
        tt_as = from_torch_dist_as(new_input, ref_tt, device=ttnn_mesh_device)
    else:
        tt_as = from_torch_dist_as(new_input, ref_tt)

    assert _topology_signature(tt_as) == _topology_signature(ref_tt)
    assert tt_as.dtype == dtype
    assert tt_as.memory_config() == memory_config
    if is_host_ref:
        torch_auto = to_torch_auto_compose(tt_as, device=ttnn_mesh_device)
    else:
        torch_auto = to_torch_auto_compose(tt_as)
    assert torch.equal(torch_auto, new_input)