diff --git a/examples/recipes/impira_layoutlm-document-qa/qnn/npu/question-answering_fp16_config.json b/examples/recipes/impira_layoutlm-document-qa/qnn/npu/question-answering_fp16_config.json new file mode 100644 index 000000000..d540f6d7f --- /dev/null +++ b/examples/recipes/impira_layoutlm-document-qa/qnn/npu/question-answering_fp16_config.json @@ -0,0 +1,124 @@ +{ + "export": { + "opset_version": 17, + "batch_size": 1, + "export_params": true, + "do_constant_folding": true, + "verbose": false, + "dynamo": false, + "enable_hierarchy_tags": true, + "clean_onnx": false, + "hierarchy_tag_format": "full", + "input_tensors": [ + { + "name": "input_ids", + "dtype": "int32", + "shape": [ + 1, + 512 + ], + "value_range": [ + 0, + 50265 + ] + }, + { + "name": "bbox", + "dtype": "int32", + "shape": [ + 1, + 512, + 4 + ], + "value_range": [ + 0, + 1000 + ] + }, + { + "name": "attention_mask", + "dtype": "int32", + "shape": [ + 1, + 512 + ], + "value_range": [ + 0, + 2 + ] + }, + { + "name": "token_type_ids", + "dtype": "int32", + "shape": [ + 1, + 512 + ], + "value_range": [ + 0, + 1 + ] + } + ], + "output_tensors": [ + { + "name": "start_logits" + }, + { + "name": "end_logits" + } + ], + "compatibility": { + "transformers_attention": "eager" + } + }, + "optim": { + "clamp_constant_values": true, + "gelu_fusion": false, + "matmul_add_fusion": false + }, + "quant": { + "mode": "fp16", + "samples": 10, + "calibration_method": "minmax", + "weight_type": "uint8", + "activation_type": "uint8", + "per_channel": false, + "symmetric": false, + "weight_symmetric": null, + "activation_symmetric": null, + "save_calibration": false, + "distribution": "uniform", + "seed": null, + "calibration_load_path": null, + "calibration_save_path": null, + "op_types_to_quantize": null, + "nodes_to_exclude": null, + "task": "question-answering", + "model_id": "impira/layoutlm-document-qa", + "model_type": "layoutlm", + "fp16_keep_io_types": true, + "fp16_op_block_list": null + }, + "compile": { + "execution_provider": "qnn", + "provider_options": { + "device_type": "NPU", + "htp_performance_mode": "burst", + "htp_graph_finalization_optimization_mode": "3" + }, + "provider_option_file_keys": [], + "enable_ep_context": true, + "embed_context": false, + "compiler": "ort", + "qnn_sdk_root": null, + "device": "npu", + "ep_device": null, + "validate": false + }, + "loader": { + "task": "question-answering", + "model_class": "LayoutLMForQuestionAnswering", + "model_type": "layoutlm" + } +} diff --git a/examples/recipes/impira_layoutlm-document-qa/qnn/npu/question-answering_w8a16_config.json b/examples/recipes/impira_layoutlm-document-qa/qnn/npu/question-answering_w8a16_config.json new file mode 100644 index 000000000..e1eb34164 --- /dev/null +++ b/examples/recipes/impira_layoutlm-document-qa/qnn/npu/question-answering_w8a16_config.json @@ -0,0 +1,122 @@ +{ + "export": { + "opset_version": 17, + "batch_size": 1, + "export_params": true, + "do_constant_folding": true, + "verbose": false, + "dynamo": false, + "enable_hierarchy_tags": true, + "clean_onnx": false, + "hierarchy_tag_format": "full", + "input_tensors": [ + { + "name": "input_ids", + "dtype": "int32", + "shape": [ + 1, + 512 + ], + "value_range": [ + 0, + 50265 + ] + }, + { + "name": "bbox", + "dtype": "int32", + "shape": [ + 1, + 512, + 4 + ], + "value_range": [ + 0, + 1000 + ] + }, + { + "name": "attention_mask", + "dtype": "int32", + "shape": [ + 1, + 512 + ], + "value_range": [ + 0, + 2 + ] + }, + { + "name": "token_type_ids", + "dtype": "int32", + "shape": [ + 1, + 512 + ], + "value_range": [ + 0, + 1 + ] + } + ], + "output_tensors": [ + { + "name": "start_logits" + }, + { + "name": "end_logits" + } + ], + "compatibility": { + "transformers_attention": "eager" + } + }, + "optim": { + "clamp_constant_values": true, + "gelu_fusion": true, + "matmul_add_fusion": false + }, + "quant": { + "mode": "static", + "samples": 10, + "calibration_method": "minmax", + "weight_type": "uint8", + "activation_type": "uint16", + "per_channel": false, + "symmetric": false, + "weight_symmetric": null, + "activation_symmetric": null, + "save_calibration": false, + "distribution": "uniform", + "seed": 42, + "calibration_load_path": null, + "calibration_save_path": null, + "op_types_to_quantize": null, + "nodes_to_exclude": null, + "task": "question-answering", + "model_id": "impira/layoutlm-document-qa", + "model_type": "layoutlm" + }, + "compile": { + "execution_provider": "qnn", + "provider_options": { + "device_type": "NPU", + "htp_performance_mode": "burst", + "htp_graph_finalization_optimization_mode": "3" + }, + "provider_option_file_keys": [], + "enable_ep_context": true, + "embed_context": false, + "compiler": "ort", + "qnn_sdk_root": null, + "device": "npu", + "ep_device": null, + "validate": false + }, + "loader": { + "task": "question-answering", + "model_class": "LayoutLMForQuestionAnswering", + "model_type": "layoutlm" + } +} diff --git a/src/winml/modelkit/build/common.py b/src/winml/modelkit/build/common.py index 863a74a21..c1b96480a 100644 --- a/src/winml/modelkit/build/common.py +++ b/src/winml/modelkit/build/common.py @@ -22,7 +22,7 @@ from ..analyze import analyze_onnx from ..compiler import compile_onnx from ..onnx import copy_onnx_model, is_quantized_onnx -from ..optim import optimize_onnx +from ..optim import WinMLOptimizationConfig, optimize_onnx from ..quant import quantize_onnx @@ -385,6 +385,7 @@ def _run_analyze_loop( analyze_iterations = 0 analyze_black_nodes = 0 discovered_optim: dict[str, bool] = {} + explicitly_disabled_optim = {name for name, enabled in config.optim.items() if enabled is False} analysis = None _not_converged = False @@ -426,6 +427,16 @@ def _run_analyze_loop( if on_patterns_discovered is not None: on_patterns_discovered(optim_config) + optim_config = WinMLOptimizationConfig.from_dict( + { + name: enabled + for name, enabled in optim_config.items() + if name not in explicitly_disabled_optim + } + ) + if not optim_config: + break + # Notify: re-optimizing with discovered flags if on_reoptimize is not None: on_reoptimize(optim_config) diff --git a/src/winml/modelkit/commands/build.py b/src/winml/modelkit/commands/build.py index a56d9f52e..2d1feaed5 100644 --- a/src/winml/modelkit/commands/build.py +++ b/src/winml/modelkit/commands/build.py @@ -1155,7 +1155,13 @@ def _patch_device(cfg: WinMLBuildConfig) -> None: provider = cfg.compile.ep_config.provider patched = WinMLCompileConfig.for_provider(provider, device=runtime_device) if patched is not None: - cfg.compile = patched + cfg.compile.ep_config.provider = patched.ep_config.provider + cfg.compile.ep_config.device = patched.ep_config.device + cfg.compile.ep_config.provider_options = { + **cfg.compile.ep_config.provider_options, + **patched.ep_config.provider_options, + } + cfg.compile.ep_device = patched.ep_device if isinstance(config_or_configs, list): for _cfg in config_or_configs: diff --git a/src/winml/modelkit/models/hf/layoutlm.py b/src/winml/modelkit/models/hf/layoutlm.py index 8085eb0ef..42b29c34b 100644 --- a/src/winml/modelkit/models/hf/layoutlm.py +++ b/src/winml/modelkit/models/hf/layoutlm.py @@ -13,14 +13,15 @@ from optimum.utils.input_generators import DummyBboxInputGenerator, DummyVisionInputGenerator from ...export import MaxLengthTextInputGenerator, register_onnx_overwrite +from .roberta import _adjust_position_embeddings if TYPE_CHECKING: import torch -class ZeroTokenTypeLayoutLMTextInputGenerator(MaxLengthTextInputGenerator): - """LayoutLM text dummy generator that keeps token_type_ids within type_vocab_size=1.""" +class LayoutLMTextInputGenerator(MaxLengthTextInputGenerator): + """LayoutLM text dummy generator constrained by the configured token-type vocabulary.""" def generate( self, @@ -29,8 +30,18 @@ def generate( int_dtype: str = "int64", float_dtype: str = "fp32", ) -> torch.Tensor: - """Generate LayoutLM text inputs, replacing token_type_ids with zeros.""" - tensor = cast( + """Generate LayoutLM text inputs within the configured token-type range.""" + if input_name == "token_type_ids": + return cast( + "torch.Tensor", + self.random_int_tensor( + [self.batch_size, self.sequence_length], + max_value=self.normalized_config.type_vocab_size, + framework=framework, + dtype=int_dtype, + ), + ) + return cast( "torch.Tensor", super().generate( input_name, @@ -39,9 +50,6 @@ def generate( float_dtype=float_dtype, ), ) - if input_name == "token_type_ids": - return tensor.new_zeros(tensor.shape) - return tensor @register_onnx_overwrite("layoutlm", "question-answering", library_name="transformers") @@ -59,10 +67,15 @@ class LayoutLMQAIOConfig(LayoutLMOnnxConfig): # type: ignore[misc] # optimum b # recipe's `value_range` instead. NORMALIZED_CONFIG_CLASS = NormalizedTextConfig.with_args( sequence_length="max_position_embeddings", + type_vocab_size="type_vocab_size", allow_new=True, ) DUMMY_INPUT_GENERATOR_CLASSES: tuple[type[Any], ...] = ( - ZeroTokenTypeLayoutLMTextInputGenerator, + LayoutLMTextInputGenerator, DummyVisionInputGenerator, DummyBboxInputGenerator, ) + + def __init__(self, config: Any, task: str, **kwargs: Any) -> None: + _adjust_position_embeddings(config) + super().__init__(config, task, **kwargs) diff --git a/tests/unit/build/test_common.py b/tests/unit/build/test_common.py index 356c560ac..4225ce2e0 100644 --- a/tests/unit/build/test_common.py +++ b/tests/unit/build/test_common.py @@ -15,7 +15,7 @@ import pytest -from winml.modelkit.build.common import run_optimize_analyze_loop +from winml.modelkit.build.common import _run_analyze_loop, run_optimize_analyze_loop from winml.modelkit.config import WinMLBuildConfig @@ -53,6 +53,25 @@ def _errored_analysis(): return AnalyzeResult(lint=lint, optimization_config=opt) +def _analysis_with_optim(**flags: bool): + """AnalyzeResult with successful lint and the requested autoconf flags.""" + from winml.modelkit.analyze import AnalyzeResult, LintResult + from winml.modelkit.optim import WinMLOptimizationConfig + + opt = WinMLOptimizationConfig(**flags) + lint = LintResult( + errors=0, + warnings=0, + info=0, + passed=True, + error_patterns=[], + warning_patterns=[], + information=[], + optimization_config=opt, + ) + return AnalyzeResult(lint=lint, optimization_config=opt) + + def _patched_loop(tmp_path: Path): """Patch the loop's stage functions to avoid real optimize/analyze/copy.""" return ( @@ -71,6 +90,115 @@ def _patched_loop(tmp_path: Path): ) +def _run_discovered_optim( + tmp_path: Path, + *, + configured: dict[str, bool], + discovered: dict[str, bool], +): + """Run the real analyze loop with deterministic discovered flags.""" + optimized = tmp_path / "optimized.onnx" + optimized.write_text("mock") + config = _config() + config.optim.update(configured) + analysis = _analysis_with_optim(**discovered) + + with ( + patch( + "winml.modelkit.build.common.analyze_onnx", + return_value=analysis, + ), + patch( + "winml.modelkit.build.common.copy_onnx_model", + side_effect=lambda src, dst: Path(dst).write_text("mock"), + ), + patch("winml.modelkit.build.common.optimize_onnx") as reoptimize, + ): + _, _, details = _run_analyze_loop( + optimized_path=optimized, + ep="qnn", + device="npu", + max_optim_iterations=1, + config=config, + ) + + return config, reoptimize, details + + +class TestAutoconfOptimPrecedence: + """Explicit sparse optimization settings take precedence over autoconf.""" + + def test_explicit_false_vetoes_discovered_true(self, tmp_path: Path) -> None: + config, reoptimize, details = _run_discovered_optim( + tmp_path, + configured={"matmul_add_fusion": False}, + discovered={"matmul_add_fusion": True}, + ) + + assert config.optim["matmul_add_fusion"] is False + reoptimize.assert_not_called() + assert details["autoconf"] == {} + + def test_unspecified_discovered_optim_applies(self, tmp_path: Path) -> None: + config, reoptimize, details = _run_discovered_optim( + tmp_path, + configured={}, + discovered={"gelu_fusion": True}, + ) + + assert config.optim["gelu_fusion"] is True + assert reoptimize.call_args.kwargs["gelu_fusion"] is True + assert details["autoconf"] == {"gelu_fusion": True} + + def test_mixed_sparse_config_vetoes_only_matching_false(self, tmp_path: Path) -> None: + config, reoptimize, details = _run_discovered_optim( + tmp_path, + configured={ + "clamp_constant_values": True, + "matmul_add_fusion": False, + }, + discovered={ + "gelu_fusion": True, + "matmul_add_fusion": True, + }, + ) + + assert config.optim == { + "clamp_constant_values": True, + "gelu_fusion": True, + "matmul_add_fusion": False, + } + assert reoptimize.call_args.kwargs["gelu_fusion"] is True + assert "matmul_add_fusion" not in reoptimize.call_args.kwargs + assert details["autoconf"] == {"gelu_fusion": True} + + def test_auto_false_skips_analyze_loop(self, tmp_path: Path) -> None: + model = tmp_path / "in.onnx" + model.write_text("mock") + optimized = tmp_path / "out.onnx" + config = _config() + config.auto = False + + with ( + patch( + "winml.modelkit.build.common.optimize_onnx", + side_effect=lambda **kw: Path(kw["output"]).write_text("mock"), + ), + patch("winml.modelkit.build.common.analyze_onnx") as analyze, + ): + result = run_optimize_analyze_loop( + model_path=model, + optimized_path=optimized, + config=config, + ep="qnn", + device="npu", + max_optim_iterations=1, + ) + + assert result[2:] == (0, 0, {}) + analyze.assert_not_called() + + class TestAllowUnsupportedNodesGate: """The unsupported-nodes RuntimeError is gated by allow_unsupported_nodes.""" @@ -97,8 +225,11 @@ def test_warns_instead_of_raising_when_allowed(self, tmp_path: Path, caplog) -> optimized = tmp_path / "out.onnx" p_opt, p_analyze, p_copy = _patched_loop(tmp_path) - with p_opt, p_analyze, p_copy, caplog.at_level( - "WARNING", logger="winml.modelkit.build.common" + with ( + p_opt, + p_analyze, + p_copy, + caplog.at_level("WARNING", logger="winml.modelkit.build.common"), ): result = run_optimize_analyze_loop( model_path=model, diff --git a/tests/unit/commands/test_build.py b/tests/unit/commands/test_build.py index 37f6bde79..5450a905c 100644 --- a/tests/unit/commands/test_build.py +++ b/tests/unit/commands/test_build.py @@ -616,9 +616,158 @@ def test_precision_fp16_sets_fp16_algorithm( [*self._base_args(cfg, tmp_path), "--device", "npu", "--precision", "fp16"] ) assert result.exit_code == 0, result.output - quant = mock_run_single_build.call_args.kwargs["config"].quant + config = mock_run_single_build.call_args.kwargs["config"] + quant = config.quant assert quant is not None assert quant.mode == "fp16" + assert config.compile is None + + def test_precision_override_preserves_existing_compile_settings( + self, tmp_path: Path, mock_run_single_build: MagicMock + ): + """Explicit precision retargets rather than replacing recipe compile settings.""" + qnn_sdk_root = tmp_path / "qnn-sdk" + provider_option_file = tmp_path / "backend.json" + qnn_sdk_root.mkdir() + provider_option_file.write_text("{}") + compile_section = { + "execution_provider": "qnn", + "provider_options": { + "device_type": "GPU", + "htp_performance_mode": "burst", + "htp_graph_finalization_optimization_mode": "3", + "backend_path": str(provider_option_file), + }, + "provider_option_file_keys": ["backend_path"], + "enable_ep_context": True, + "embed_context": True, + "compiler": "qairt", + "qnn_sdk_root": str(qnn_sdk_root), + "device": "gpu", + "ep_device": { + "ep": "QNNExecutionProvider", + "device": "gpu", + }, + "validate": False, + "verbose": True, + } + cfg = _make_minimal_config_file(tmp_path, compile_section=compile_section) + + result = _invoke( + [*self._base_args(cfg, tmp_path), "--device", "npu", "--precision", "fp16"] + ) + + assert result.exit_code == 0, result.output + compile_config = mock_run_single_build.call_args.kwargs["config"].compile + assert compile_config is not None + assert compile_config.ep_config.provider == "qnn" + assert compile_config.ep_config.device == "npu" + assert compile_config.ep_config.provider_options == { + "device_type": "NPU", + "htp_performance_mode": "burst", + "htp_graph_finalization_optimization_mode": "3", + "backend_path": str(provider_option_file), + } + assert compile_config.ep_config.provider_option_file_keys == {"backend_path"} + assert compile_config.ep_config.enable_ep_context is True + assert compile_config.ep_config.embed_context is True + assert compile_config.ep_config.compiler == "qairt" + assert compile_config.ep_config.qnn_sdk_root == qnn_sdk_root + assert compile_config.ep_device is None + assert compile_config.validate is False + assert compile_config.verbose is True + + def test_without_target_override_preserves_existing_compile_settings( + self, tmp_path: Path, mock_run_single_build: MagicMock + ): + """Loading a recipe without a target override leaves its compile section unchanged.""" + compile_section = { + "execution_provider": "qnn", + "provider_options": { + "device_type": "GPU", + "htp_performance_mode": "sustained_high_performance", + }, + "device": "gpu", + "validate": False, + } + cfg = _make_minimal_config_file(tmp_path, compile_section=compile_section) + + result = _invoke(self._base_args(cfg, tmp_path)) + + assert result.exit_code == 0, result.output + compile_config = mock_run_single_build.call_args.kwargs["config"].compile + assert compile_config is not None + assert compile_config.ep_config.device == "gpu" + assert compile_config.ep_config.provider_options == { + "device_type": "GPU", + "htp_performance_mode": "sustained_high_performance", + } + assert compile_config.validate is False + + def test_target_override_preserves_compile_settings_for_module_configs(self, tmp_path: Path): + """The target patch applies to every config in a module recipe.""" + module_configs = [ + { + "loader": { + "task": "image-classification", + "model_type": "resnet", + "model_class": "ResNetModel", + "module_path": module_path, + }, + "export": {"opset_version": 17, "batch_size": 1}, + "optim": {}, + "quant": None, + "compile": { + "execution_provider": "qnn", + "provider_options": { + "device_type": "GPU", + "htp_performance_mode": "burst", + }, + "device": "gpu", + "validate": False, + }, + } + for module_path in ("layer1", "layer2") + ] + config_path = tmp_path / "modules.json" + config_path.write_text(json.dumps(module_configs)) + build_result = MagicMock( + reused=True, + output_dir=tmp_path / "module-output", + elapsed=0.1, + ) + + with ( + patch( + "winml.modelkit.commands.build._build_modules", + return_value=[build_result, build_result], + ) as mock_build_modules, + patch("winml.modelkit.build.write_module_summary"), + ): + result = _invoke( + [ + "-c", + str(config_path), + "-o", + str(tmp_path / "out"), + "--device", + "npu", + "--precision", + "fp16", + ] + ) + + assert result.exit_code == 0, result.output + patched_configs = mock_build_modules.call_args.kwargs["configs"] + assert len(patched_configs) == 2 + for patched_config in patched_configs: + assert patched_config.compile is not None + assert patched_config.compile.ep_config.device == "npu" + assert patched_config.compile.ep_config.provider_options == { + "device_type": "NPU", + "htp_performance_mode": "burst", + } + assert patched_config.compile.validate is False def test_precision_alone_triggers_quant_patch( self, tmp_path: Path, mock_run_single_build: MagicMock diff --git a/tests/unit/export/test_onnx_config_overrides.py b/tests/unit/export/test_onnx_config_overrides.py index 9102d2c1c..b224c5791 100644 --- a/tests/unit/export/test_onnx_config_overrides.py +++ b/tests/unit/export/test_onnx_config_overrides.py @@ -138,21 +138,31 @@ def test_bert_io_specs_shape_matches(self, bert_config) -> None: class TestLayoutLMQuestionAnsweringOverride: """LayoutLM QA export must include bbox and safe token_type_ids.""" - def test_layoutlm_qa_dummy_inputs_include_bbox_and_zero_token_types(self) -> None: - """Dummy inputs must keep bbox while forcing token_type_ids to zero.""" + @staticmethod + def _config( + *, + max_position_embeddings: int = 32, + pad_token_id: int = 0, + type_vocab_size: int = 1, + ): from transformers import LayoutLMConfig - layoutlm_config = LayoutLMConfig( + return LayoutLMConfig( vocab_size=100, hidden_size=64, num_hidden_layers=2, num_attention_heads=2, intermediate_size=128, - max_position_embeddings=32, + max_position_embeddings=max_position_embeddings, max_2d_position_embeddings=1024, - type_vocab_size=1, + pad_token_id=pad_token_id, + type_vocab_size=type_vocab_size, ) + def test_layoutlm_qa_dummy_inputs_include_bbox_and_zero_token_types(self) -> None: + """A single token type keeps valid zero-only dummy inputs.""" + layoutlm_config = self._config() + inputs = generate_dummy_inputs("layoutlm", "question-answering", layoutlm_config) assert set(inputs) == {"input_ids", "bbox", "attention_mask", "token_type_ids"} @@ -163,18 +173,7 @@ def test_layoutlm_qa_dummy_inputs_include_bbox_and_zero_token_types(self) -> Non def test_layoutlm_qa_io_specs_include_span_outputs(self) -> None: """LayoutLM QA specs expose document bbox input and span logits outputs.""" - from transformers import LayoutLMConfig - - layoutlm_config = LayoutLMConfig( - vocab_size=100, - hidden_size=64, - num_hidden_layers=2, - num_attention_heads=2, - intermediate_size=128, - max_position_embeddings=32, - max_2d_position_embeddings=1024, - type_vocab_size=1, - ) + layoutlm_config = self._config() specs = resolve_io_specs("layoutlm", "question-answering", layoutlm_config) @@ -186,6 +185,53 @@ def test_layoutlm_qa_io_specs_include_span_outputs(self) -> None: ] assert specs["input_shapes"] == [(1, 32), (1, 32, 4), (1, 32), (1, 32)] assert specs["output_names"] == ["start_logits", "end_logits"] + assert specs["value_ranges"]["token_type_ids"] == (0, 1) + + def test_layoutlm_qa_uses_usable_roberta_style_sequence_length(self) -> None: + """A 514-position, pad-offset config has capacity for 512 input tokens.""" + layoutlm_config = self._config(max_position_embeddings=514, pad_token_id=1) + + specs = resolve_io_specs("layoutlm", "question-answering", layoutlm_config) + + assert specs["input_shapes"] == [(1, 512), (1, 512, 4), (1, 512), (1, 512)] + + def test_layoutlm_qa_keeps_bert_style_sequence_length(self) -> None: + """A zero-offset LayoutLM config keeps its full position capacity.""" + layoutlm_config = self._config(max_position_embeddings=512, pad_token_id=0) + + specs = resolve_io_specs("layoutlm", "question-answering", layoutlm_config) + + assert specs["input_shapes"] == [(1, 512), (1, 512, 4), (1, 512), (1, 512)] + + def test_layoutlm_qa_preserves_explicit_sequence_length(self) -> None: + """An explicit content budget remains authoritative for every coupled input.""" + layoutlm_config = self._config(max_position_embeddings=514, pad_token_id=1) + + specs = resolve_io_specs( + "layoutlm", + "question-answering", + layoutlm_config, + sequence_length=128, + ) + + assert specs["input_shapes"] == [(1, 128), (1, 128, 4), (1, 128), (1, 128)] + + def test_layoutlm_qa_derives_token_types_from_type_vocab_size(self) -> None: + """Dummy token types use the configured high-exclusive vocabulary bound.""" + import torch + + layoutlm_config = self._config(type_vocab_size=4) + + with torch.random.fork_rng(): + torch.manual_seed(0) + inputs = generate_dummy_inputs("layoutlm", "question-answering", layoutlm_config) + specs = resolve_io_specs("layoutlm", "question-answering", layoutlm_config) + + token_type_ids = inputs["token_type_ids"] + assert torch.count_nonzero(token_type_ids).item() > 0 + assert token_type_ids.min().item() >= 0 + assert token_type_ids.max().item() < layoutlm_config.type_vocab_size + assert specs["value_ranges"]["token_type_ids"] == (0, 4) class TestLayoutLMv3QuestionAnsweringOverride: