Download code/models/common/tests/modules/mlp/test_mlp_2d.py from tt-hous/clef: direct link, hf CLI and curl.
- Browser
- Download file 18.3 kB
-
https://huggingface.co/tt-hous/clef/resolve/main/code/models/common/tests/modules/mlp/test_mlp_2d.py
- Command line
-
hf download hf://tt-hous/clef/code/models/common/tests/modules/mlp/test_mlp_2d.py
-
curl -L -o test_mlp_2d.py https://huggingface.co/tt-hous/clef/resolve/main/code/models/common/tests/modules/mlp/test_mlp_2d.py
18.3 kB
| # SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| """ | |
| Tests for the MLP2D module (TG/Galaxy 2D mesh topology). | |
| This test suite verifies: | |
| 1. Unit tests for config dataclasses (no device needed) | |
| 2. MLP2D class matches HuggingFace/Meta reference model | |
| 3. MLP2D correctly rejects non-TG devices | |
| 4. Backward compatibility: MLP2D.from_model_args() works correctly | |
| """ | |
| from unittest.mock import MagicMock | |
| import pytest | |
| import torch | |
| from loguru import logger | |
| from transformers import AutoConfig, AutoModelForCausalLM | |
| # transformers 5.x moved no_init_weights to transformers.initialization; fall back | |
| # to the old location for transformers < 5.x. | |
| try: | |
| from transformers.initialization import no_init_weights | |
| except ImportError: | |
| from transformers.modeling_utils import no_init_weights | |
| import ttnn | |
| from models.common.modules.lazy_weight import LazyWeight | |
| from models.common.modules.mlp.mlp_2d import MLP2D, MLP2DConfig, _resolve_mlp2d_config | |
| from models.common.utility_functions import comp_allclose, comp_pcc | |
| # ============================================================================ | |
| # Unit Tests - No device required | |
| # ============================================================================ | |
| def create_mock_lazy_weight(device=None, shape=None): | |
| w = MagicMock(spec=LazyWeight) | |
| w.device = device | |
| w.source = MagicMock() | |
| if shape: | |
| w.source.shape = shape | |
| return w | |
| def test_mlp_2d_config_creation(): | |
| """Test that MLP2DConfig dataclass can be created with explicit values. | |
| Note: _resolve_mlp2d_config is tested via integration tests (test_mlp_2d_vs_reference) | |
| since it requires real devices and tt_ccl. This test only verifies dataclass creation. | |
| """ | |
| # Mock device | |
| mock_device = MagicMock(spec=ttnn.MeshDevice) | |
| mock_device.shape = (4, 8) | |
| mock_device.get_num_devices.return_value = 32 | |
| mock_device.dram_grid_size.return_value = ttnn.CoreCoord(12, 1) | |
| # Mock tt_ccl (required for unit tests since we can't create real semaphores) | |
| mock_tt_ccl = MagicMock() | |
| # Mock weights | |
| w1 = create_mock_lazy_weight(device=mock_device, shape=(8192, 28672)) | |
| w2 = create_mock_lazy_weight(device=mock_device, shape=(28672, 8192)) | |
| w3 = create_mock_lazy_weight(device=mock_device, shape=(8192, 28672)) | |
| # Create config with explicit values (like MLP1D unit test pattern) | |
| config = MLP2DConfig( | |
| w1=w1, | |
| w2=w2, | |
| w3=w3, | |
| mesh_device=mock_device, | |
| tt_ccl=mock_tt_ccl, | |
| dim=8192, | |
| hidden_dim=28672, | |
| max_batch_size=32, | |
| ) | |
| # Verify explicit values are preserved | |
| assert config.w1 is w1 | |
| assert config.w2 is w2 | |
| assert config.w3 is w3 | |
| assert config.mesh_device is mock_device | |
| assert config.tt_ccl is mock_tt_ccl | |
| assert config.dim == 8192 | |
| assert config.hidden_dim == 28672 | |
| assert config.max_batch_size == 32 | |
| # Verify defaults for optional fields | |
| assert config.w1_w3_dtype is None # Will be resolved to bfloat8_b | |
| assert config.topology is None # Will be auto-detected | |
| def test_mlp_2d_config_rejects_1d_mesh(): | |
| """Test that MLP2DConfig raises assertion error for 1D mesh (requires 2D mesh).""" | |
| # Mock 1D device | |
| mock_device_1d = MagicMock(spec=ttnn.MeshDevice) | |
| mock_device_1d.shape = (1, 8) | |
| w1 = create_mock_lazy_weight(device=mock_device_1d, shape=(4096, 14336)) | |
| w2 = create_mock_lazy_weight(device=mock_device_1d, shape=(14336, 4096)) | |
| w3 = create_mock_lazy_weight(device=mock_device_1d, shape=(4096, 14336)) | |
| config = MLP2DConfig(w1=w1, w2=w2, w3=w3) | |
| with pytest.raises(AssertionError, match="MLP2D requires 2D mesh"): # allow-pytest.raises: pre-existing | |
| _resolve_mlp2d_config(config) | |
| def test_mlp_2d_optimization_config(): | |
| """Test MLP2D optimization settings can be explicitly set. | |
| Note: _resolve_mlp2d_config is tested via integration tests. This test only | |
| verifies that optimization config fields can be explicitly set on the dataclass. | |
| """ | |
| mock_device = MagicMock(spec=ttnn.MeshDevice) | |
| mock_device.shape = (4, 8) | |
| mock_device.get_num_devices.return_value = 32 | |
| mock_tt_ccl = MagicMock() | |
| w1 = create_mock_lazy_weight(device=mock_device, shape=(8192, 28672)) | |
| w2 = create_mock_lazy_weight(device=mock_device, shape=(28672, 8192)) | |
| w3 = create_mock_lazy_weight(device=mock_device, shape=(8192, 28672)) | |
| # Create config with explicit dtype overrides | |
| config = MLP2DConfig( | |
| w1=w1, | |
| w2=w2, | |
| w3=w3, | |
| mesh_device=mock_device, | |
| tt_ccl=mock_tt_ccl, | |
| dim=8192, | |
| hidden_dim=28672, | |
| w1_w3_dtype=ttnn.bfloat16, | |
| activation_dtype=ttnn.bfloat16, | |
| ) | |
| # Verify explicit values are preserved | |
| assert config.w1_w3_dtype == ttnn.bfloat16 | |
| assert config.activation_dtype == ttnn.bfloat16 | |
| assert config.w2_dtype is None # Will be resolved to bfloat8_b default | |
| def test_mlp_2d_rejects_non_galaxy_from_model_args(cluster_shape): | |
| """ | |
| Test that MLP2D.from_model_args() raises ValueError for non-Galaxy devices. | |
| """ | |
| class _DummyArgs: | |
| def __init__(self, cluster_shape): | |
| self.cluster_shape = list(cluster_shape) | |
| model_args = _DummyArgs(cluster_shape) | |
| with pytest.raises(ValueError, match="MLP2D requires Galaxy topology"): # allow-pytest.raises: pre-existing | |
| MLP2D.from_model_args( | |
| mesh_device=None, | |
| tt_ccl=None, | |
| args=model_args, | |
| state_dict=None, | |
| weight_cache_path=None, | |
| layer_num=0, | |
| ) | |
| # ============================================================================ | |
| # TTNN Topology Bug Tests - Document known issues with 2D mesh tensor topology | |
| # ============================================================================ | |
| def _check_topology_has_duplicate_shard_dims(placements: list) -> tuple[bool, str]: | |
| """ | |
| Check if placements have duplicate shard dimensions (the known bug pattern). | |
| Args: | |
| placements: List of placement objects from tensor_topology().placements() | |
| Returns: | |
| (has_duplicate, message): Tuple of (True if duplicate dims found, descriptive message) | |
| """ | |
| def normalize_dim(d: int, ndim: int = 4) -> int: | |
| return d if d >= 0 else d + ndim | |
| axis0_dim = placements[0].dim if isinstance(placements[0], ttnn.PlacementShard) else None | |
| axis1_dim = placements[1].dim if isinstance(placements[1], ttnn.PlacementShard) else None | |
| if axis0_dim is not None and axis1_dim is not None: | |
| norm_axis0 = normalize_dim(axis0_dim) | |
| norm_axis1 = normalize_dim(axis1_dim) | |
| if norm_axis0 == norm_axis1: | |
| return True, ( | |
| f"Both mesh axes shard the same tensor dimension: " | |
| f"axis0={axis0_dim} (norm={norm_axis0}), axis1={axis1_dim} (norm={norm_axis1})" | |
| ) | |
| return False, "Topology appears correct" | |
| def ttnn_linear_2d_mesh_has_topology_bug(ttnn_mesh_device): | |
| """ | |
| Fixture that checks if the ttnn.linear 2D mesh topology bug exists. | |
| This fixture runs a minimal topology check and returns the result. | |
| Other tests can use this to decide whether to apply workarounds. | |
| Note: scope="function" because ttnn_mesh_device may vary per test parametrization. | |
| The check is fast so the overhead is minimal. | |
| Returns: | |
| bool: True if the bug is present, False if fixed | |
| """ | |
| mesh_device = ttnn_mesh_device | |
| cluster_shape = list(mesh_device.shape) | |
| # Skip if not a 2D mesh | |
| if len(cluster_shape) != 2 or cluster_shape[0] == 1 or cluster_shape[1] == 1: | |
| logger.info("Not a 2D mesh, skipping topology bug check") | |
| return False | |
| dim, hidden_dim, seq_len = 4096, 14336, 32 | |
| # Create minimal test tensors | |
| torch_input = torch.randn(1, 1, seq_len, dim, dtype=torch.bfloat16) | |
| tt_input = ttnn.from_torch( | |
| torch_input, | |
| device=mesh_device, | |
| mesh_mapper=ttnn.ShardTensor2dMesh(mesh_device, dims=(None, 3), mesh_shape=cluster_shape), | |
| dtype=ttnn.bfloat16, | |
| memory_config=ttnn.DRAM_MEMORY_CONFIG, | |
| layout=ttnn.TILE_LAYOUT, | |
| ) | |
| torch_weight = torch.randn(dim, hidden_dim, dtype=torch.bfloat16) | |
| tt_weight = ttnn.from_torch( | |
| torch_weight, | |
| device=mesh_device, | |
| mesh_mapper=ttnn.ShardTensor2dMesh(mesh_device, dims=(-1, -2), mesh_shape=cluster_shape), | |
| dtype=ttnn.bfloat16, | |
| memory_config=ttnn.DRAM_MEMORY_CONFIG, | |
| layout=ttnn.TILE_LAYOUT, | |
| ) | |
| # Run linear and check topology | |
| tt_output = ttnn.linear(tt_input, tt_weight) | |
| output_placements = list(tt_output.tensor_topology().placements()) | |
| has_bug, msg = _check_topology_has_duplicate_shard_dims(output_placements) | |
| if has_bug: | |
| logger.warning(f"ttnn.linear 2D mesh topology bug detected: {msg}") | |
| else: | |
| logger.info("ttnn.linear 2D mesh topology bug NOT detected - may be fixed!") | |
| # Cleanup | |
| ttnn.deallocate(tt_output) | |
| ttnn.deallocate(tt_input) | |
| ttnn.deallocate(tt_weight) | |
| return has_bug | |
| def test_ttnn_linear_2d_mesh_topology_bug(ttnn_linear_2d_mesh_has_topology_bug: bool): | |
| """ | |
| Document the TTNN bug where ttnn.linear produces incorrect topology metadata | |
| for 2D mesh matmul operations. | |
| Setup (in fixture): | |
| - Input x: shape [1, 1, 32, 4096], topology [Replicated, Shard(3)] | |
| - Weight w: shape [4096, 14336], topology [Shard(-1), Shard(-2)] | |
| Expected output topology after x @ w: | |
| - [Shard(3), PartialSum] or similar | |
| Actual (buggy) output topology: | |
| - [Shard(-1), Shard(3)] - both axes claim to shard the same dimension! | |
| TODO: File TTNN issue and remove xfail once fixed. | |
| """ | |
| if ttnn_linear_2d_mesh_has_topology_bug: | |
| pytest.fail( | |
| "ttnn.linear produces invalid topology: both mesh axes shard the same dimension. " | |
| "Expected different dimensions or [Shard, PartialSum/Replicate]." | |
| ) | |
| # [INFO] currently tt_transformers is not testing 2D mesh MLP in CI -- existing TG tests are DP only that runs 1D MLPs in parallel | |
| # todo)) add more targeted unit tests like the ones in test_mlp_1d.py when relevant model are implemented | |
| def test_mlp_2d_vs_reference( | |
| ttnn_mesh_device: ttnn.MeshDevice, | |
| ttnn_linear_2d_mesh_has_topology_bug: bool, | |
| seq_len, | |
| mode, | |
| dtype, | |
| batch_size, | |
| dim, | |
| hidden_dim, | |
| hf_model_name, | |
| ): | |
| """ | |
| Test MLP2D constructed via direct APIs (MLP2DConfig) matches HF reference MLP. | |
| """ | |
| seed = 1234 | |
| torch.manual_seed(seed) | |
| # Load HF config and create model with dummy weights | |
| config = AutoConfig.from_pretrained(hf_model_name) | |
| config.num_hidden_layers = 1 | |
| with no_init_weights(): | |
| hf_model = AutoModelForCausalLM.from_config(config, torch_dtype=torch.bfloat16) | |
| reference_mlp = hf_model.model.layers[0].mlp | |
| # Initialize only the MLP submodule deterministically. | |
| with torch.no_grad(): | |
| for param in reference_mlp.parameters(): | |
| param.copy_(torch.randn_like(param)) | |
| assert dim == config.hidden_size | |
| assert hidden_dim == config.intermediate_size | |
| cluster_shape = list(ttnn_mesh_device.shape) | |
| # TT expects weights in (input_dim, output_dim) layout. | |
| w1_torch = reference_mlp.gate_proj.weight.T # (dim, hidden_dim) | |
| w3_torch = reference_mlp.up_proj.weight.T # (dim, hidden_dim) | |
| w2_torch = reference_mlp.down_proj.weight.T # (hidden_dim, dim) | |
| # [INFO] PyTorch's nn.Linear operates on the last dimension regardless of tensor rank. | |
| torch_input = torch.randn(batch_size, 1, seq_len, dim, dtype=torch.bfloat16) | |
| # Create LazyWeights | |
| ttnn.SetDefaultDevice(ttnn_mesh_device) | |
| lazy_w1 = LazyWeight(source=w1_torch, dtype=dtype) | |
| lazy_w2 = LazyWeight(source=w2_torch, dtype=dtype) | |
| lazy_w3 = LazyWeight(source=w3_torch, dtype=dtype) | |
| # Create MLP2D directly with weights | |
| tt_model = MLP2D(lazy_w1, lazy_w2, lazy_w3) | |
| # Run HF reference MLP | |
| with torch.no_grad(): | |
| reference_output = reference_mlp(torch_input) | |
| # Run TT model | |
| # [INFO] we use LazyWeight on input for the benefit of faster testing (cached input); in production, the input is already a ttnn tensor. | |
| tt_input = LazyWeight(source=torch_input, dtype=ttnn.bfloat8_b) | |
| tt_output = tt_model.forward(tt_input, mode) | |
| ttnn.SetDefaultDevice(None) | |
| # WORKAROUND: ttnn.linear produces incorrect topology metadata for 2D mesh matmul. | |
| # The output topology shows [Shard(-1), Shard(3)] but the correct data layout after | |
| # the final all-reduce on axis 0 is [Replicated, Shard(3)]: | |
| # expected: [ttnn.PlacementReplicate, ttnn.PlacementShard(3)] | |
| # got: [ttnn.PlacementShard(-1), ttnn.PlacementShard(3)] | |
| # - Axis 0 (size 8): Replicated (all-reduced/gathered) | |
| # - Axis 1 (size 4): Sharded on dim 3 | |
| # The fixture `ttnn_linear_2d_mesh_has_topology_bug` checks this once per module. | |
| if ttnn_linear_2d_mesh_has_topology_bug: | |
| # Bug present: use explicit mesh_composer with correct topology | |
| expected_composer_cfg = ttnn.MeshComposerConfig( | |
| dims=[0, 3], # axis 0: replicated (dim ignored), axis 1: shard on dim 3 | |
| mesh_shape_override=ttnn.MeshShape([1, cluster_shape[1]]), # [1, 4]: skip axis 0, concat axis 1 | |
| ) | |
| mesh_composer = ttnn.create_mesh_composer(ttnn_mesh_device, expected_composer_cfg) | |
| tt_output_torch = ttnn.to_torch(tt_output, mesh_composer=mesh_composer) | |
| else: | |
| raise RuntimeError("Bug fixed: use auto_compose -- tt_output_torch = to_torch_auto_compose(tt_output)") | |
| # Compare | |
| pcc_required = 0.99 | |
| passing, pcc_message = comp_pcc(reference_output, tt_output_torch, pcc_required) | |
| logger.info(comp_allclose(reference_output, tt_output_torch)) | |
| logger.info(f"MLP2D (direct API) vs HF reference: {pcc_message}") | |
| assert passing, f"MLP2D output does not meet PCC requirement {pcc_required}: {pcc_message}." | |
| logger.info(f"MLP2D (direct API) vs HF reference: PASSED for mode={mode}, seq_len={seq_len}") | |
| # [INFO] this test will retire once models/tt_transformers/tt/model_config.py retires | |
| def test_mlp_2d_vs_reference_from_model_args(ttnn_mesh_device: ttnn.MeshDevice, seq_len): | |
| """ | |
| Test that MLP2D class matches the HuggingFace/Meta reference model. | |
| Runs only on Galaxy (TG) devices due to Galaxy-specific CCL operations. | |
| """ | |
| import os | |
| from models.tt_transformers.tests.test_utils import get_ref_model_dype | |
| from models.tt_transformers.tt.ccl import TT_CCL | |
| from models.tt_transformers.tt.model_config import ModelArgs | |
| batch_size = 1 | |
| mode = "decode" if seq_len <= 32 else "prefill" | |
| os.environ.setdefault("HF_MODEL", "meta-llama/Llama-3.1-8B-Instruct") | |
| model_args = ModelArgs(ttnn_mesh_device, max_batch_size=batch_size, max_seq_len=128, cache_hf=True) | |
| model_args.n_layers = 1 | |
| state_dict = model_args.load_state_dict() | |
| # Load reference model | |
| first_layer_prefix = model_args.get_state_dict_prefix("MLP", 0) | |
| partial_state_dict = { | |
| k[len(first_layer_prefix) + 1 :]: v for k, v in state_dict.items() if k.startswith(first_layer_prefix) | |
| } | |
| reference_model = model_args.reference_mlp() | |
| reference_model.load_state_dict(partial_state_dict) | |
| # Create MLP2D | |
| tt_ccl = TT_CCL(ttnn_mesh_device) | |
| tt_model = MLP2D.from_model_args( | |
| mesh_device=ttnn_mesh_device, | |
| tt_ccl=tt_ccl, | |
| args=model_args, | |
| state_dict=state_dict, | |
| weight_cache_path=model_args.weight_cache_path(ttnn.bfloat8_b), | |
| layer_num=0, | |
| ) | |
| # Create input | |
| torch_input = torch.randn( | |
| 1, 1, seq_len, model_args.dim, dtype=get_ref_model_dype(reference_model, model_args.model_name) | |
| ) | |
| # Run reference | |
| reference_output = reference_model(torch_input) | |
| # Run TT model | |
| input_mem_config = ttnn.DRAM_MEMORY_CONFIG | |
| tt_input = ttnn.from_torch( | |
| torch_input, | |
| device=ttnn_mesh_device, | |
| mesh_mapper=ttnn.ShardTensor2dMesh(ttnn_mesh_device, dims=(None, 3), mesh_shape=model_args.cluster_shape), | |
| dtype=ttnn.bfloat8_b, | |
| memory_config=input_mem_config, | |
| layout=ttnn.TILE_LAYOUT, | |
| ) | |
| tt_output = tt_model.forward(tt_input, mode) | |
| tt_output_torch = ttnn.to_torch( | |
| tt_output, | |
| mesh_composer=ttnn.ConcatMesh2dToTensor(ttnn_mesh_device, dims=(1, 3), mesh_shape=model_args.cluster_shape), | |
| ) | |
| tt_output_torch = tt_output_torch[:, :1, :, :] | |
| # Compare | |
| pcc_required = 0.99 | |
| passing, pcc_message = comp_pcc(reference_output, tt_output_torch, pcc_required) | |
| logger.info(comp_allclose(reference_output, tt_output_torch)) | |
| logger.info(f"MLP2D vs reference: {pcc_message}") | |
| assert passing, f"MLP2D output does not meet PCC requirement {pcc_required}: {pcc_message}." | |
| logger.info(f"MLP2D vs reference: PASSED for mode={mode}, seq_len={seq_len}") | |