wtpsplit 2.2.1__tar.gz → 2.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/PKG-INFO +42 -1
  2. wtpsplit-2.2.1/wtpsplit.egg-info/PKG-INFO → wtpsplit-2.2.2/README.md +38 -37
  3. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/pyproject.toml +5 -1
  4. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/setup.py +2 -1
  5. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/__init__.py +43 -1
  6. wtpsplit-2.2.2/wtpsplit/aitune_integration.py +279 -0
  7. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/extract.py +133 -14
  8. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/models.py +16 -4
  9. wtpsplit-2.2.2/wtpsplit/train/hf_compat.py +31 -0
  10. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/train/transformers_trainer_imports.py +8 -4
  11. wtpsplit-2.2.1/README.md → wtpsplit-2.2.2/wtpsplit.egg-info/PKG-INFO +78 -0
  12. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit.egg-info/SOURCES.txt +1 -2
  13. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit.egg-info/requires.txt +4 -0
  14. wtpsplit-2.2.1/wtpsplit/data/dummy_lora_smoke.pth +0 -0
  15. wtpsplit-2.2.1/wtpsplit/data/priors_generated.py +0 -250
  16. wtpsplit-2.2.1/wtpsplit/train/hf_compat.py +0 -18
  17. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/LICENSE +0 -0
  18. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/setup.cfg +0 -0
  19. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/configs.py +0 -0
  20. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/data/__init__.py +0 -0
  21. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/data/language_info.csv +0 -0
  22. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/data/punctuation.json +0 -0
  23. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/data/punctuation.txt +0 -0
  24. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/data/punctuation_xlmr.txt +0 -0
  25. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/data/punctuation_xlmr_unk.txt +0 -0
  26. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/data/sentence_stats.json +0 -0
  27. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/data_acquisition/__init__.py +0 -0
  28. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/data_acquisition/extract_all_data.py +0 -0
  29. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/data_acquisition/extract_eval_data.py +0 -0
  30. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/data_acquisition/extract_sentences.py +0 -0
  31. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/data_acquisition/extract_shared_task_data.py +0 -0
  32. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/evaluation/__init__.py +0 -0
  33. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/evaluation/adapt.py +0 -0
  34. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/evaluation/evaluate_sepp_nlg_subtask1.py +0 -0
  35. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/evaluation/extrinsic.py +0 -0
  36. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/evaluation/intrinsic_baselines.py +0 -0
  37. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/evaluation/intrinsic_baselines_multilingual.py +0 -0
  38. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/evaluation/intrinsic_pairwise.py +0 -0
  39. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/evaluation/intrinsic_ted.py +0 -0
  40. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/evaluation/legal_baselines.py +0 -0
  41. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/evaluation/llm_sentence.py +0 -0
  42. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/evaluation/punct_annotation.py +0 -0
  43. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/evaluation/punct_annotation_wtp.py +0 -0
  44. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/extract_batched.py +0 -0
  45. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/train/__init__.py +0 -0
  46. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/train/adaptertrainer.py +0 -0
  47. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/train/evaluate.py +0 -0
  48. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/train/train.py +0 -0
  49. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/train/train_SM.py +0 -0
  50. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/train/train_lora.py +0 -0
  51. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/train/trainer.py +0 -0
  52. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/train/utils.py +0 -0
  53. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/utils/__init__.py +0 -0
  54. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/utils/clean_tweets.py +0 -0
  55. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/utils/constraints.py +0 -0
  56. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/utils/create_dummy_data.py +0 -0
  57. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/utils/download_spacy.py +0 -0
  58. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/utils/priors.py +0 -0
  59. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit/utils/remove_unks.py +0 -0
  60. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit.egg-info/dependency_links.txt +0 -0
  61. {wtpsplit-2.2.1 → wtpsplit-2.2.2}/wtpsplit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wtpsplit
3
- Version: 2.2.1
3
+ Version: 2.2.2
4
4
  Summary: Universal Robust, Efficient and Adaptable Sentence Segmentation
5
5
  Home-page: https://github.com/segment-any-text/wtpsplit
6
6
  Author: Markus Frohmann, Igor Sterner, Benjamin Minixhofer
@@ -23,6 +23,9 @@ Provides-Extra: onnx-gpu
23
23
  Requires-Dist: onnxruntime-gpu>=1.13.1; extra == "onnx-gpu"
24
24
  Provides-Extra: onnx-cpu
25
25
  Requires-Dist: onnxruntime>=1.13.1; extra == "onnx-cpu"
26
+ Provides-Extra: aitune
27
+ Requires-Dist: aitune>=0.3.0; extra == "aitune"
28
+ Requires-Dist: requests; extra == "aitune"
26
29
  Dynamic: author
27
30
  Dynamic: author-email
28
31
  Dynamic: description
@@ -88,6 +91,44 @@ sat_adapted.split("This is a test This is another test.")
88
91
  # returns ['This is a test ', 'This is another test']
89
92
  ```
90
93
 
94
+ ## Faster PyTorch inference (`torch.compile` / TorchInductor)
95
+
96
+ For repeated inference without exporting to ONNX, compile the PyTorch model with [TorchInductor](https://pytorch.org/docs/stable/torch.compiler.html) via `optimize()` (PyTorch 2.0+). This is **optional** - the first `split` after `optimize()` can be slow while graphs are built. Call `optimize()` **after** `to()` / `half()` so the compiled graph matches *your* device and dtype.
97
+
98
+ ```python
99
+ from wtpsplit import SaT
100
+
101
+ sat = SaT("sat-3l-sm")
102
+ sat.half().to("cuda")
103
+ sat.optimize() # backend="inductor" (default); dynamic shapes on
104
+
105
+ sat.split("This is a test This is another test.")
106
+ ```
107
+
108
+ - **SaT** and **WtP** PyTorch checkpoints only — not available with `ort_providers` / ONNX.
109
+ - `backend` synonyms: `"inductor"`, `"torchinductor"`.
110
+ - Chunk length and the last batch size change between calls, so `dynamic=True` is the default. `mode="reduce-overhead"` (CUDA graphs) is faster only when every forward uses the same shapes; otherwise use the default or `mode="max-autotune-no-cudagraphs"`.
111
+ - Optional on NVIDIA Ampere+: `torch.set_float32_matmul_precision("high")` before inference (faster fp32 matmuls).
112
+
113
+ ### NVIDIA AITune (auto backend selection, CUDA)
114
+
115
+ [NVIDIA AITune](https://github.com/ai-dynamo/aitune) benchmarks backends (TensorRT, Torch-TensorRT, Torch Inductor, …) on your GPU and picks a fast path. Requires Linux, CUDA, and a separate install:
116
+
117
+ ```bash
118
+ pip install wtpsplit[aitune] --extra-index-url https://pypi.nvidia.com
119
+ ```
120
+
121
+ ```python
122
+ sat = SaT("sat-3l-sm")
123
+ sat.half().to("cuda")
124
+ # first_wins tries TensorRT → Torch-TensorRT → Inductor when available
125
+ sat.optimize(backend="aitune")
126
+ # or only Inductor via AITune (faster tuning, no TensorRT dependency):
127
+ # sat.optimize(backend="aitune", aitune_strategy="inductor_only", aitune_max_batches=4)
128
+
129
+ sat.split("This is a test This is another test.")
130
+ ```
131
+
91
132
  ## ONNX Support
92
133
 
93
134
  🚀 You can now enable even faster ONNX inference for `sat` and `sat-sm` models! 🚀
@@ -1,40 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: wtpsplit
3
- Version: 2.2.1
4
- Summary: Universal Robust, Efficient and Adaptable Sentence Segmentation
5
- Home-page: https://github.com/segment-any-text/wtpsplit
6
- Author: Markus Frohmann, Igor Sterner, Benjamin Minixhofer
7
- Author-email: markus.frohmann@gmail.com
8
- License: MIT
9
- Requires-Python: >=3.9
10
- Description-Content-Type: text/markdown
11
- License-File: LICENSE
12
- Requires-Dist: transformers>=4.22.2
13
- Requires-Dist: huggingface-hub
14
- Requires-Dist: numpy>=1.0
15
- Requires-Dist: scikit-learn>=1
16
- Requires-Dist: tqdm
17
- Requires-Dist: skops
18
- Requires-Dist: pandas>=1
19
- Requires-Dist: mosestokenizer
20
- Provides-Extra: adapters
21
- Requires-Dist: adapters>=1.0.1; extra == "adapters"
22
- Provides-Extra: onnx-gpu
23
- Requires-Dist: onnxruntime-gpu>=1.13.1; extra == "onnx-gpu"
24
- Provides-Extra: onnx-cpu
25
- Requires-Dist: onnxruntime>=1.13.1; extra == "onnx-cpu"
26
- Dynamic: author
27
- Dynamic: author-email
28
- Dynamic: description
29
- Dynamic: description-content-type
30
- Dynamic: home-page
31
- Dynamic: license
32
- Dynamic: license-file
33
- Dynamic: provides-extra
34
- Dynamic: requires-dist
35
- Dynamic: requires-python
36
- Dynamic: summary
37
-
38
1
  <h1 align="center">wtpsplit🪓</h1>
39
2
  <h3 align="center">Segment any Text - Robustly, Efficiently, Adaptably⚡</h3>
40
3
 
@@ -88,6 +51,44 @@ sat_adapted.split("This is a test This is another test.")
88
51
  # returns ['This is a test ', 'This is another test']
89
52
  ```
90
53
 
54
+ ## Faster PyTorch inference (`torch.compile` / TorchInductor)
55
+
56
+ For repeated inference without exporting to ONNX, compile the PyTorch model with [TorchInductor](https://pytorch.org/docs/stable/torch.compiler.html) via `optimize()` (PyTorch 2.0+). This is **optional** - the first `split` after `optimize()` can be slow while graphs are built. Call `optimize()` **after** `to()` / `half()` so the compiled graph matches *your* device and dtype.
57
+
58
+ ```python
59
+ from wtpsplit import SaT
60
+
61
+ sat = SaT("sat-3l-sm")
62
+ sat.half().to("cuda")
63
+ sat.optimize() # backend="inductor" (default); dynamic shapes on
64
+
65
+ sat.split("This is a test This is another test.")
66
+ ```
67
+
68
+ - **SaT** and **WtP** PyTorch checkpoints only — not available with `ort_providers` / ONNX.
69
+ - `backend` synonyms: `"inductor"`, `"torchinductor"`.
70
+ - Chunk length and the last batch size change between calls, so `dynamic=True` is the default. `mode="reduce-overhead"` (CUDA graphs) is faster only when every forward uses the same shapes; otherwise use the default or `mode="max-autotune-no-cudagraphs"`.
71
+ - Optional on NVIDIA Ampere+: `torch.set_float32_matmul_precision("high")` before inference (faster fp32 matmuls).
72
+
73
+ ### NVIDIA AITune (auto backend selection, CUDA)
74
+
75
+ [NVIDIA AITune](https://github.com/ai-dynamo/aitune) benchmarks backends (TensorRT, Torch-TensorRT, Torch Inductor, …) on your GPU and picks a fast path. Requires Linux, CUDA, and a separate install:
76
+
77
+ ```bash
78
+ pip install wtpsplit[aitune] --extra-index-url https://pypi.nvidia.com
79
+ ```
80
+
81
+ ```python
82
+ sat = SaT("sat-3l-sm")
83
+ sat.half().to("cuda")
84
+ # first_wins tries TensorRT → Torch-TensorRT → Inductor when available
85
+ sat.optimize(backend="aitune")
86
+ # or only Inductor via AITune (faster tuning, no TensorRT dependency):
87
+ # sat.optimize(backend="aitune", aitune_strategy="inductor_only", aitune_max_batches=4)
88
+
89
+ sat.split("This is a test This is another test.")
90
+ ```
91
+
91
92
  ## ONNX Support
92
93
 
93
94
  🚀 You can now enable even faster ONNX inference for `sat` and `sat-sm` models! 🚀
@@ -4,7 +4,11 @@ target-version = ["py39", "py310", "py311", "py312"]
4
4
 
5
5
  [tool.ruff]
6
6
  line-length = 120
7
- lint.ignore = ["E741"]
7
+
8
+ [tool.ruff.lint]
9
+ # Pre-0.16 default. Ruff 0.16 enables 413 rules by default; CI installs latest ruff.
10
+ select = ["E4", "E7", "E9", "F"]
11
+ ignore = ["E741"]
8
12
 
9
13
  [tool.ruff.lint.per-file-ignores]
10
14
  "test.py" = ["E501"]
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="wtpsplit",
5
- version="2.2.1",
5
+ version="2.2.2",
6
6
  packages=find_packages(),
7
7
  description="Universal Robust, Efficient and Adaptable Sentence Segmentation",
8
8
  long_description=open("README.md", encoding="utf-8").read(),
@@ -25,6 +25,7 @@ setup(
25
25
  "adapters": ["adapters>=1.0.1"],
26
26
  "onnx-gpu": ["onnxruntime-gpu>=1.13.1"],
27
27
  "onnx-cpu": ["onnxruntime>=1.13.1"],
28
+ "aitune": ["aitune>=0.3.0", "requests"],
28
29
  },
29
30
  url="https://github.com/segment-any-text/wtpsplit",
30
31
  package_data={"wtpsplit": ["data/*"]},
@@ -30,7 +30,7 @@ from wtpsplit.utils.constraints import (
30
30
  )
31
31
  from wtpsplit.utils.priors import create_prior_function
32
32
 
33
- __version__ = "2.2.1"
33
+ __version__ = "2.2.2"
34
34
 
35
35
  # suppress docopt syntax warnings (triggered in Python 3.14+)
36
36
  warnings.filterwarnings("ignore", category=SyntaxWarning, module="docopt")
@@ -228,6 +228,27 @@ class WtP:
228
228
  assert hasattr(self, "model")
229
229
  return getattr(self.model, name)
230
230
 
231
+ def optimize(
232
+ self,
233
+ backend: str = "inductor",
234
+ *,
235
+ mode: str = None,
236
+ fullgraph: bool = False,
237
+ dynamic: bool = True,
238
+ **compile_kwargs,
239
+ ):
240
+ """Compile the PyTorch model with ``torch.compile`` (TorchInductor by default). Not available for ONNX."""
241
+ if not isinstance(self.model, PyTorchWrapper):
242
+ raise ValueError(
243
+ "optimize() only applies to PyTorch models loaded from a checkpoint. "
244
+ "It does not apply to ONNX Runtime (set ort_providers=None) or to ad-hoc model objects "
245
+ "that are not wrapped as expected."
246
+ )
247
+ self.model.optimize(
248
+ backend=backend, mode=mode, fullgraph=fullgraph, dynamic=dynamic, **compile_kwargs
249
+ )
250
+ return self
251
+
231
252
  def predict_proba(
232
253
  self,
233
254
  text_or_texts,
@@ -908,6 +929,27 @@ class SaT:
908
929
  assert hasattr(self, "model")
909
930
  return getattr(self.model, name)
910
931
 
932
+ def optimize(
933
+ self,
934
+ backend: str = "inductor",
935
+ *,
936
+ mode: str = None,
937
+ fullgraph: bool = False,
938
+ dynamic: bool = True,
939
+ **compile_kwargs,
940
+ ):
941
+ """Compile the PyTorch model with ``torch.compile`` (TorchInductor by default). Not available for ONNX."""
942
+ if not isinstance(self.model, PyTorchWrapper):
943
+ raise ValueError(
944
+ "optimize() only applies to PyTorch models loaded from a checkpoint. "
945
+ "It does not apply to ONNX Runtime (set ort_providers=None) or to ad-hoc model objects "
946
+ "that are not wrapped as expected."
947
+ )
948
+ self.model.optimize(
949
+ backend=backend, mode=mode, fullgraph=fullgraph, dynamic=dynamic, **compile_kwargs
950
+ )
951
+ return self
952
+
911
953
  def predict_proba(
912
954
  self,
913
955
  text_or_texts,
@@ -0,0 +1,279 @@
1
+ """Optional NVIDIA AITune integration for :meth:`PyTorchWrapper.optimize`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def _require_aitune():
9
+ try:
10
+ import aitune.torch as ait # noqa: F401
11
+ except ImportError as e:
12
+ raise ImportError(
13
+ "backend='aitune' requires NVIDIA AITune. Install with:\n"
14
+ " pip install wtpsplit[aitune] --extra-index-url https://pypi.nvidia.com"
15
+ ) from e
16
+ return ait
17
+
18
+
19
+ def _build_calibration_dataset(model, device) -> list[dict[str, Any]]:
20
+ """Synthetic unbatched samples covering the chunk lengths ``extract`` actually feeds.
21
+
22
+ AITune stacks these on dim 0, so each tensor is one example: ``(seq,)`` or
23
+ ``(seq, num_hashes)``. Lengths stay inside the position-embedding window and
24
+ on the character-model downsampling grid.
25
+ """
26
+ import torch
27
+
28
+ config = model.config
29
+ model_type = getattr(config, "model_type", "") or ""
30
+ rate = int(getattr(config, "downsampling_rate", 1) or 1)
31
+ lengths = tuple(length for length in (64, 128, 256, 512) if length % rate == 0) or (rate,)
32
+ uses_lang = getattr(config, "language_adapter", "off") in {"on", "shared"}
33
+ samples: list[dict[str, Any]] = []
34
+
35
+ for seq_len in lengths:
36
+ if "xlm" in model_type:
37
+ # CLS/SEP are added around the chunk; stay within the 512 position window.
38
+ length = min(seq_len + 2, 512)
39
+ sample = {
40
+ "input_ids": torch.randint(0, 50000, (length,), dtype=torch.long, device=device),
41
+ "attention_mask": torch.ones(length, dtype=torch.float32, device=device),
42
+ }
43
+ else:
44
+ num_hashes = getattr(config, "num_hash_functions", 8)
45
+ num_buckets = getattr(config, "num_hash_buckets", 10000)
46
+ sample = {
47
+ "hashed_ids": torch.randint(0, num_buckets, (seq_len, num_hashes), dtype=torch.long, device=device),
48
+ "attention_mask": torch.ones(seq_len, dtype=torch.float32, device=device),
49
+ }
50
+ if uses_lang and "xlm" not in model_type:
51
+ # Batched to shape (batch,) by AITune, matching ``extract``.
52
+ sample["language_ids"] = torch.zeros((), dtype=torch.long, device=device)
53
+ samples.append(sample)
54
+
55
+ return samples
56
+
57
+
58
+ def _backend_cls(*names: str):
59
+ for name in names:
60
+ factory = _try_backend("aitune.torch.backend", name)
61
+ if factory is not None:
62
+ return factory
63
+ raise ImportError(
64
+ "Could not import an AITune TorchInductor backend "
65
+ f"(tried {', '.join(names)}). Upgrade with: pip install -U 'aitune>=0.4'"
66
+ )
67
+
68
+
69
+ def _strategy_cls(*names: str):
70
+ for name in names:
71
+ factory = _try_backend("aitune.torch.tune_strategy", name)
72
+ if factory is not None:
73
+ return factory
74
+ raise ImportError(
75
+ "Could not import an AITune tune strategy "
76
+ f"(tried {', '.join(names)}). Upgrade with: pip install -U 'aitune>=0.4'"
77
+ )
78
+
79
+
80
+ def _inductor_backend(*, mode: str | None, fullgraph: bool, dynamic: bool):
81
+ """TorchInductor backend, using the current JIT class and falling back to older names."""
82
+ backend_cls = _backend_cls("TorchInductorJitBackend", "TorchInductorBackend")
83
+ config_cls = _try_backend("aitune.torch.backend", "TorchInductorJitBackendConfig")
84
+ if config_cls is None:
85
+ return backend_cls()
86
+ config_kwargs: dict[str, Any] = {"fullgraph": fullgraph, "dynamic": dynamic}
87
+ if mode is not None:
88
+ config_kwargs["mode"] = mode
89
+ try:
90
+ return backend_cls(config=config_cls(**config_kwargs))
91
+ except TypeError:
92
+ return backend_cls()
93
+
94
+
95
+ def _resolve_strategy(
96
+ strategy_name: str | None,
97
+ backends: list | None,
98
+ *,
99
+ mode: str | None,
100
+ fullgraph: bool,
101
+ dynamic: bool,
102
+ ):
103
+ first_wins = _strategy_cls("FirstWinsStrategy")
104
+ one_backend = _strategy_cls("OneBackendStrategy")
105
+
106
+ if backends is not None:
107
+ return first_wins(backends=backends)
108
+
109
+ key = (strategy_name or "first_wins").lower().replace("-", "_")
110
+ inductor = _inductor_backend(mode=mode, fullgraph=fullgraph, dynamic=dynamic)
111
+ if key in ("inductor", "inductor_only", "torch_inductor"):
112
+ return one_backend(backend=inductor)
113
+
114
+ available = _default_aitune_backends(inductor)
115
+ if key in ("highest_throughput", "best", "max_throughput"):
116
+ strategy_cls = _strategy_cls("MaxThroughputStrategy", "HighestThroughputStrategy")
117
+ try:
118
+ return strategy_cls(backends=available)
119
+ except TypeError:
120
+ return strategy_cls()
121
+
122
+ return first_wins(backends=available)
123
+
124
+
125
+ def _default_aitune_backends(inductor_backend) -> list:
126
+ """Backends tried in order for ``first_wins`` (TensorRT first when importable)."""
127
+ backends = []
128
+ for class_name in ("TensorRTBackend", "TorchTensorRTJitBackend"):
129
+ factory = _try_backend("aitune.torch.backend", class_name)
130
+ if factory is not None:
131
+ backends.append(factory())
132
+ backends.append(inductor_backend)
133
+ return backends
134
+
135
+
136
+ def _try_backend(module_name: str, class_name: str):
137
+ try:
138
+ import importlib
139
+
140
+ mod = importlib.import_module(module_name)
141
+ return getattr(mod, class_name)
142
+ except Exception:
143
+ return None
144
+
145
+
146
+ def pop_aitune_kwargs(compile_kwargs: dict[str, Any]) -> dict[str, Any]:
147
+ """Extract AITune-specific keys from ``compile_kwargs`` (mutates the dict)."""
148
+ keys = (
149
+ "aitune_batch_sizes",
150
+ "aitune_max_batches",
151
+ "aitune_strategy",
152
+ "aitune_backends",
153
+ "aitune_dry_run",
154
+ "aitune_calibration",
155
+ )
156
+ out = {}
157
+ for key in keys:
158
+ if key in compile_kwargs:
159
+ out[key] = compile_kwargs.pop(key)
160
+ return out
161
+
162
+
163
+ def apply_aitune(model, *, mode: str | None = None, fullgraph: bool = False, dynamic: bool = True, **aitune_kwargs):
164
+ """Wrap ``model`` with AITune and run ahead-of-time tuning. Returns the wrapped module."""
165
+ import logging
166
+
167
+ import torch
168
+
169
+ ait = _require_aitune()
170
+ from aitune.torch.dataloader import DynamicShapeDataset
171
+
172
+ logger = logging.getLogger(__name__)
173
+
174
+ if not torch.cuda.is_available():
175
+ raise RuntimeError(
176
+ "AITune requires a CUDA GPU. Move the model with `.to('cuda')` before `optimize(backend='aitune')`."
177
+ )
178
+
179
+ device = getattr(model, "device", None)
180
+ if device is None or getattr(device, "type", None) != "cuda":
181
+ raise RuntimeError(
182
+ "AITune requires the model on CUDA. Call `.to('cuda')` before `optimize(backend='aitune')`."
183
+ )
184
+
185
+ model.eval()
186
+ calibration = aitune_kwargs.pop("aitune_calibration", None)
187
+ dataset = (
188
+ calibration if calibration is not None else DynamicShapeDataset(_build_calibration_dataset(model, device))
189
+ )
190
+ batch_sizes = aitune_kwargs.pop("aitune_batch_sizes", None) or [1, 2]
191
+ max_batches = aitune_kwargs.pop("aitune_max_batches", 4)
192
+ strategy_name = aitune_kwargs.pop("aitune_strategy", None)
193
+ backends = aitune_kwargs.pop("aitune_backends", None)
194
+ dry_run = aitune_kwargs.pop("aitune_dry_run", False)
195
+ if len(set(batch_sizes)) < 2:
196
+ logger.warning(
197
+ "AITune needs at least two batch sizes to mark the batch axis as dynamic; "
198
+ "using [1, 2] so later split() batch sizes still match."
199
+ )
200
+ batch_sizes = [1, 2]
201
+
202
+ strategy = _resolve_strategy(
203
+ strategy_name, backends, mode=mode, fullgraph=fullgraph, dynamic=dynamic
204
+ )
205
+ name = f"{model.__class__.__module__}.{model.__class__.__qualname__}".replace("/", "_")
206
+ wrapped = ait.Module(model, name=name, strategy=strategy)
207
+
208
+ ait.tune(
209
+ wrapped,
210
+ dataset,
211
+ batch_sizes=batch_sizes,
212
+ max_num_batches_per_batch_size=max_batches,
213
+ device=device,
214
+ dry_run=dry_run,
215
+ )
216
+ if not dry_run:
217
+ _install_metadata_fallback(wrapped)
218
+ return wrapped
219
+
220
+
221
+ def _install_metadata_fallback(wrapped):
222
+ """Allow AITune dynamic-shape metadata to match runtime samples.
223
+
224
+ AITune records dynamic axes during tuning, but its runtime lookup can still
225
+ require exact ``SampleMetadata`` dictionary membership. SaT/WtP chunk lengths
226
+ vary naturally, so fall back to the compatible ``TensorSpec.matches`` check
227
+ before reporting that no backend was found.
228
+ """
229
+ import types
230
+
231
+ from aitune.torch.module.sample_metadata import SampleMetadata
232
+
233
+ tuned = getattr(wrapped, "_self_wrapper", None)
234
+ if tuned is None or not hasattr(tuned, "_backends"):
235
+ return
236
+
237
+ def metadata_matches(expected, actual):
238
+ if expected == actual:
239
+ return True
240
+ if expected.llm_phase != actual.llm_phase:
241
+ return False
242
+ if expected.other_data != actual.other_data:
243
+ return False
244
+
245
+ expected_specs = {spec.name: spec for spec in expected.tensor_specs}
246
+ actual_specs = {spec.name: spec for spec in actual.tensor_specs}
247
+ if expected_specs.keys() != actual_specs.keys():
248
+ return False
249
+
250
+ for name, actual_spec in actual_specs.items():
251
+ expected_spec = expected_specs[name]
252
+ if expected_spec.dtype != actual_spec.dtype:
253
+ return False
254
+ if not expected_spec.matches(actual_spec):
255
+ return False
256
+ return True
257
+
258
+ def safe_call_backend_with_dynamic_match(self, sample):
259
+ args, kwargs = sample
260
+ # AITune 0.6+ takes one named-input dict. Older releases took (args, kwargs).
261
+ if hasattr(self, "_forward_signature"):
262
+ forward_inputs = self._forward_signature.normalize(args, kwargs)
263
+ sample_metadata = SampleMetadata.from_inputs(
264
+ forward_inputs.arguments, strict=self._config.strict_mode
265
+ )
266
+ else:
267
+ sample_metadata = SampleMetadata.from_inputs(args, kwargs, strict=self._config.strict_mode)
268
+ backend = self._backends.get(sample_metadata)
269
+ if backend is None:
270
+ for candidate_metadata, candidate_backend in self._backends.items():
271
+ if metadata_matches(candidate_metadata, sample_metadata):
272
+ backend = candidate_backend
273
+ break
274
+ if backend is None:
275
+ raise RuntimeError(self.ERROR_NO_BACKEND_FOUND.format(sample_metadata))
276
+ return backend.infer(*args, **kwargs)
277
+
278
+ tuned.safe_call_backend = types.MethodType(safe_call_backend_with_dynamic_match, tuned)
279
+ tuned._backend_func = tuned.safe_call_backend