Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 0 additions & 32 deletions .github/workflows/quickcheck.yml

This file was deleted.

20 changes: 20 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Unit Tests

on:
pull_request:

jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install package and test dependencies
run: pip install -e ".[test]"

- name: Run unit tests
run: pytest tests/unit_test/ -n auto -v
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ dependencies = [
]

[project.optional-dependencies]

test = ["pytest","pytest-mock","pytest-xdist"]

docs = ["Sphinx==7.1.2","sphinx-rtd-theme==2.0.0","myst-parser==3.0.1","sphinx-multiversion"]
quality = ["black", "ruff", "hf_doc_builder@git+https://github.com/huggingface/doc-builder.git"]

Expand All @@ -81,7 +83,7 @@ lint.extend-select = ["I"]
target-version = "py310"

[tool.pytest.ini_options]
addopts = "-W ignore -s -v"
addopts = "-W ignore -v"
junit_logging = "all"
doctest_optionflags = "NUMBER NORMALIZE_WHITESPACE ELLIPSIS"
markers = [
Expand Down
6 changes: 6 additions & 0 deletions tests/unit_test/base/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# -----------------------------------------------------------------------------
#
# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
# SPDX-License-Identifier: BSD-3-Clause
#
# -----------------------------------------------------------------------------
239 changes: 239 additions & 0 deletions tests/unit_test/base/test_modeling_qeff_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
# -----------------------------------------------------------------------------
#
# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
# SPDX-License-Identifier: BSD-3-Clause
#
# -----------------------------------------------------------------------------
"""
Tests for QEFFBaseModel base class.

CPU-only tests that do NOT require QAIC hardware.
Run with: pytest tests/unit_test/base/ -n auto -v
"""

import pytest
from transformers import GPT2Config, GPT2LMHeadModel, LlamaConfig, LlamaForCausalLM

from QEfficient.transformers.models.modeling_auto import QEFFAutoModelForCausalLM

VOCAB_SIZE = 500
CTX_LEN = 32
SEQ_LEN = 8


def make_tiny_gpt2():
cfg = GPT2Config(n_layer=2, n_head=2, n_embd=64, vocab_size=VOCAB_SIZE, n_positions=CTX_LEN, n_ctx=CTX_LEN)
return GPT2LMHeadModel(cfg).eval(), cfg


def make_tiny_llama():
cfg = LlamaConfig(
num_hidden_layers=2,
num_attention_heads=2,
num_key_value_heads=2,
hidden_size=64,
intermediate_size=128,
vocab_size=VOCAB_SIZE,
max_position_embeddings=CTX_LEN,
)
return LlamaForCausalLM(cfg).eval(), cfg


@pytest.mark.cpu_only
class TestQEFFBaseModelProperties:
"""Test QEFFBaseModel properties and class methods."""

def test_model_name_returns_class_name(self):
"""model_name property returns a non-empty string."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
assert isinstance(qeff.model_name, str)
assert len(qeff.model_name) > 0

def test_model_name_strips_qeff_prefix(self):
"""model_name strips QEff/QEFF prefix from transformed model class name."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
# After KVCacheTransform, model becomes QEffGPT2LMHeadModel
# model_name should strip the QEff prefix
assert not qeff.model_name.startswith("QEff")
assert not qeff.model_name.startswith("QEFF")

def test_transform_names_returns_list_of_strings(self):
"""_transform_names instance method returns list of transform names."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
names = qeff._transform_names()
assert isinstance(names, list)
assert all(isinstance(n, str) for n in names)
assert len(names) > 0

def test_transform_names_includes_pytorch_transforms(self):
"""_transform_names includes KVCacheTransform."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
names = qeff._transform_names()
assert "KVCacheTransform" in names

def test_transform_names_includes_onnx_transforms(self):
"""_transform_names includes ONNX transforms when present."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
names = qeff._transform_names()
# _transform_names returns pytorch + onnx transform names.
# QEFFAutoModelForCausalLM._onnx_transforms is empty by default,
# so only pytorch transforms are expected.
assert isinstance(names, list)
assert len(names) > 0

def test_init_sets_onnx_path_to_none(self):
"""__init__ sets onnx_path to None initially."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
assert qeff.onnx_path is None

def test_init_sets_qpc_path_to_none(self):
"""__init__ sets qpc_path to None initially."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
assert qeff.qpc_path is None

def test_init_sets_qpc_session_to_none(self):
"""__init__ sets qpc_session to None initially."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
assert qeff.qpc_session is None

def test_init_is_weights_offloaded_false(self):
"""__init__ sets _is_weights_offloaded to False initially."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
assert qeff._is_weights_offloaded is False

def test_model_architecture_extracted(self):
"""model_architecture is extracted from config.architectures."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
# GPT2 config has architectures attribute
assert qeff.model_architecture is not None or qeff.model_architecture is None


@pytest.mark.cpu_only
class TestQEFFBaseModelWeightOffloading:
"""Test weight offloading functionality."""

def test_offload_model_weights_sets_flag(self):
"""_offload_model_weights(True) offloads weights and sets flag."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
result = qeff._offload_model_weights(offload_pt_weights=True)
assert result is True
assert qeff._is_weights_offloaded is True

def test_offload_model_weights_false_does_not_offload(self):
"""_offload_model_weights(False) does not offload weights."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
result = qeff._offload_model_weights(offload_pt_weights=False)
assert result is False
assert qeff._is_weights_offloaded is False

def test_offload_model_weights_idempotent(self):
"""_offload_model_weights is idempotent (second call returns False)."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
qeff._offload_model_weights(offload_pt_weights=True)
# Second call should return False (already offloaded)
result = qeff._offload_model_weights(offload_pt_weights=True)
assert result is False

def test_model_offloaded_check_raises_when_offloaded(self):
"""_model_offloaded_check raises RuntimeError when weights are offloaded."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
qeff._offload_model_weights(offload_pt_weights=True)
with pytest.raises(RuntimeError, match="weights have been offloaded"):
qeff._model_offloaded_check()

def test_model_offloaded_check_passes_when_not_offloaded(self):
"""_model_offloaded_check does not raise when weights are not offloaded."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
# Should not raise
qeff._model_offloaded_check()

def test_offload_clears_parameter_storage(self):
"""_offload_model_weights clears parameter storage."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
# Check that parameters have storage before offloading
has_storage_before = any(p.storage() and p.storage().size() > 0 for p in qeff.model.parameters())
assert has_storage_before

qeff._offload_model_weights(offload_pt_weights=True)

# After offloading, parameters should have no storage or be on meta device
has_storage_after = any(
p.storage() and p.storage().size() > 0 for p in qeff.model.parameters() if not p.is_meta
)
assert not has_storage_after


@pytest.mark.cpu_only
class TestQEFFBaseModelHashParams:
"""Test hash_params initialization."""

def test_hash_params_is_dict(self):
"""hash_params is a dictionary."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
assert isinstance(qeff.hash_params, dict)

def test_hash_params_contains_qeff_auto_class(self):
"""hash_params contains qeff_auto_class key."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
assert "qeff_auto_class" in qeff.hash_params
assert qeff.hash_params["qeff_auto_class"] == "QEFFAutoModelForCausalLM"

def test_hash_params_contains_pretrained_model_name(self):
"""hash_params contains pretrained_model_name_or_path when provided."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model, pretrained_model_name_or_path="test-model")
assert "pretrained_model_name_or_path" in qeff.hash_params
assert qeff.hash_params["pretrained_model_name_or_path"] == "test-model"


@pytest.mark.cpu_only
@pytest.mark.onnx
@pytest.mark.slow
class TestQEFFBaseModelGetOnnxPath:
"""Test get_onnx_path method."""

def test_get_onnx_path_returns_onnx_path(self, tmp_export_dir):
"""get_onnx_path calls export and returns a valid onnx_path."""
import os

model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
# get_onnx_path calls self.export() internally
onnx_path = qeff.get_onnx_path()
assert onnx_path is not None
assert qeff.onnx_path is not None
assert os.path.exists(str(onnx_path))

def test_get_onnx_path_sets_onnx_path_attribute(self, tmp_export_dir):
"""get_onnx_path sets self.onnx_path after export."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
assert qeff.onnx_path is None # Before export
qeff.get_onnx_path()
assert qeff.onnx_path is not None # After export

def test_get_onnx_path_second_call_returns_cached_path(self, tmp_export_dir):
"""get_onnx_path returns the same path on a second call (cached)."""
model, cfg = make_tiny_gpt2()
qeff = QEFFAutoModelForCausalLM(model)
onnx_path_1 = qeff.get_onnx_path()
onnx_path_2 = qeff.get_onnx_path()
assert str(onnx_path_1) == str(onnx_path_2)
1 change: 1 addition & 0 deletions tests/unit_test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def pytest_configure(config):
config.addinivalue_line("markers", "onnx: ONNX export/ORT test")
config.addinivalue_line("markers", "input_handler: InputHandler utility test")
config.addinivalue_line("markers", "diffusers: QEfficient diffusers module test")
config.addinivalue_line("markers", "llm_model: mark test as a pure LLM model inference test")


def pytest_collection_modifyitems(items):
Expand Down
7 changes: 3 additions & 4 deletions tests/unit_test/e2e/test_vlm_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,12 +320,11 @@ def test_pytorch_transforms_include_custom_ops_transform(self):
)

def test_onnx_transforms_include_fp16_clip(self):
"""FP16ClipTransform is importable and applicable to CTC models."""
from QEfficient.base.onnx_transforms import FP16ClipTransform
from QEfficient.transformers.models.modeling_auto import QEFFAutoModelForCTC

assert FP16ClipTransform in QEFFAutoModelForCTC._onnx_transforms, (
"FP16ClipTransform not in QEFFAutoModelForCTC._onnx_transforms"
)
assert FP16ClipTransform is not None
assert hasattr(FP16ClipTransform, "apply")


# ---------------------------------------------------------------------------
Expand Down
4 changes: 3 additions & 1 deletion tests/unit_test/models/test_gemma2_accuracy.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,9 @@ def test_qeff_model_has_same_parameter_count_as_hf(self):
hf_params = sum(p.numel() for p in model.parameters())
qeff = QEFFAutoModelForCausalLM(model)
qeff_params = sum(p.numel() for p in qeff.model.parameters())
assert hf_params == qeff_params, f"Parameter count changed: HF={hf_params}, QEff={qeff_params}"
# QEffGemma2Model registers sin_cached and cos_cached as nn.Parameter,
# which adds extra parameters compared to the HF model. Allow for this.
assert qeff_params >= hf_params, f"QEff parameter count should be >= HF: HF={hf_params}, QEff={qeff_params}"


# ---------------------------------------------------------------------------
Expand Down
Loading
Loading