mostlyai-engine 1.1.12__tar.gz → 1.2.0__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.
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/PKG-INFO +10 -11
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/__init__.py +1 -1
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_common.py +1 -1
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_language/engine/base.py +4 -6
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_language/engine/hf_engine.py +72 -29
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_language/engine/vllm_engine.py +89 -67
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_language/generation.py +13 -16
- mostlyai_engine-1.2.0/mostlyai/engine/_language/xgrammar_utils.py +147 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/pyproject.toml +14 -12
- mostlyai_engine-1.1.12/mostlyai/engine/_language/formatron_utils.py +0 -264
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/.gitignore +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/LICENSE +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/README.md +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_dtypes.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/__init__.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/language/__init__.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/language/categorical.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/language/datetime.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/language/numeric.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/language/text.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/tabular/__init__.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/tabular/categorical.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/tabular/character.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/tabular/datetime.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/tabular/itt.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/tabular/lat_long.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/tabular/numeric.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_language/__init__.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_language/common.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_language/encoding.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_language/engine/__init__.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_language/lstm.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_language/tokenizer_utils.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_language/training.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_memory.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_tabular/__init__.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_tabular/argn.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_tabular/common.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_tabular/encoding.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_tabular/fairness.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_tabular/generation.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_tabular/training.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_training_utils.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_workspace.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/analysis.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/domain.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/encoding.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/generation.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/logging.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/splitting.py +0 -0
- {mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/training.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mostlyai-engine
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Synthetic Data Engine
|
|
5
5
|
Project-URL: homepage, https://github.com/mostly-ai/mostlyai-engine
|
|
6
6
|
Project-URL: repository, https://github.com/mostly-ai/mostlyai-engine
|
|
@@ -24,26 +24,25 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
24
24
|
Classifier: Topic :: Software Development :: Libraries
|
|
25
25
|
Classifier: Typing :: Typed
|
|
26
26
|
Requires-Python: >=3.10
|
|
27
|
-
Requires-Dist: accelerate>=
|
|
28
|
-
Requires-Dist: datasets
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
Requires-Dist:
|
|
31
|
-
Requires-Dist: joblib<2,>=1.4.2
|
|
27
|
+
Requires-Dist: accelerate>=1.5.0
|
|
28
|
+
Requires-Dist: datasets>=3.0.0
|
|
29
|
+
Requires-Dist: huggingface-hub>=0.30.0
|
|
30
|
+
Requires-Dist: joblib>=1.4.2
|
|
32
31
|
Requires-Dist: json-repair>=0.30.0
|
|
33
|
-
Requires-Dist: numpy
|
|
34
|
-
Requires-Dist: opacus
|
|
32
|
+
Requires-Dist: numpy>=1.26.3
|
|
33
|
+
Requires-Dist: opacus>=1.5.2
|
|
35
34
|
Requires-Dist: pandas~=2.2.0
|
|
36
35
|
Requires-Dist: peft>=0.12.0
|
|
37
36
|
Requires-Dist: psutil<6,>=5.9.5
|
|
38
37
|
Requires-Dist: pyarrow>=16.0.0
|
|
39
|
-
Requires-Dist: rich<14,>=13.9.4
|
|
40
38
|
Requires-Dist: setuptools<76,>=75.0.0
|
|
41
|
-
Requires-Dist: tokenizers
|
|
39
|
+
Requires-Dist: tokenizers>=0.21.0
|
|
42
40
|
Requires-Dist: torch==2.6.0; sys_platform == 'linux'
|
|
43
41
|
Requires-Dist: torch>=2.6.0; sys_platform == 'darwin'
|
|
44
42
|
Requires-Dist: torchaudio==2.6.0; sys_platform == 'linux'
|
|
45
43
|
Requires-Dist: torchvision==0.21.0; sys_platform == 'linux'
|
|
46
|
-
Requires-Dist: transformers
|
|
44
|
+
Requires-Dist: transformers>=4.51.0
|
|
45
|
+
Requires-Dist: xgrammar==0.1.17
|
|
47
46
|
Provides-Extra: cpu
|
|
48
47
|
Requires-Dist: torch==2.6.0+cpu; (sys_platform == 'linux') and extra == 'cpu'
|
|
49
48
|
Requires-Dist: torch==2.6.0; (sys_platform != 'linux') and extra == 'cpu'
|
|
@@ -22,7 +22,7 @@ from mostlyai.engine.generation import generate
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
__all__ = ["split", "analyze", "encode", "train", "generate", "init_logging"]
|
|
25
|
-
__version__ = "1.
|
|
25
|
+
__version__ = "1.2.0"
|
|
26
26
|
|
|
27
27
|
# suppress specific warning related to os.fork() in multi-threaded processes
|
|
28
28
|
warnings.filterwarnings("ignore", category=DeprecationWarning, message=".*multi-threaded.*fork.*")
|
|
@@ -511,7 +511,7 @@ def skip_if_error(func: Callable) -> Callable:
|
|
|
511
511
|
|
|
512
512
|
|
|
513
513
|
def encode_slen_sidx_sdec(vals: pd.Series, max_seq_len: int, prefix: str = "") -> pd.DataFrame:
|
|
514
|
-
assert vals
|
|
514
|
+
assert is_integer_dtype(vals)
|
|
515
515
|
if max_seq_len < SLEN_SIDX_DIGIT_ENCODING_THRESHOLD or prefix == SDEC_SUB_COLUMN_PREFIX:
|
|
516
516
|
# encode slen and sidx as numeric_discrete
|
|
517
517
|
df = pd.DataFrame({f"{prefix}cat": vals})
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
from collections.abc import Generator
|
|
15
17
|
from dataclasses import dataclass
|
|
16
|
-
from abc import abstractmethod, ABC
|
|
17
|
-
from collections.abc import Callable
|
|
18
18
|
|
|
19
|
-
from
|
|
19
|
+
from pydantic import BaseModel
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
@dataclass
|
|
@@ -27,9 +27,7 @@ class EngineMetrics:
|
|
|
27
27
|
|
|
28
28
|
class LanguageEngine(ABC):
|
|
29
29
|
@abstractmethod
|
|
30
|
-
def initialize_logits_processors(
|
|
31
|
-
self, formatter_builders: list[FormatterBuilder], vocab_processors: list[Callable] | None = None
|
|
32
|
-
):
|
|
30
|
+
def initialize_logits_processors(self, schemas: Generator[BaseModel]):
|
|
33
31
|
pass
|
|
34
32
|
|
|
35
33
|
@abstractmethod
|
{mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_language/engine/hf_engine.py
RENAMED
|
@@ -12,22 +12,73 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
-
from
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
16
17
|
import time
|
|
17
|
-
from collections.abc import
|
|
18
|
+
from collections.abc import Generator
|
|
19
|
+
from os import PathLike
|
|
18
20
|
from pathlib import Path
|
|
19
21
|
|
|
20
22
|
import torch
|
|
21
|
-
|
|
23
|
+
import transformers
|
|
24
|
+
import xgrammar as xgr
|
|
22
25
|
from peft import PeftModel
|
|
23
|
-
|
|
26
|
+
from pydantic import BaseModel
|
|
24
27
|
from transformers import AutoTokenizer
|
|
28
|
+
|
|
25
29
|
from mostlyai.engine._language.common import load_base_model_and_config
|
|
26
|
-
from mostlyai.engine._language.
|
|
30
|
+
from mostlyai.engine._language.engine.base import EngineMetrics, LanguageEngine
|
|
27
31
|
from mostlyai.engine._language.tokenizer_utils import tokenize_fn
|
|
32
|
+
from mostlyai.engine._language.xgrammar_utils import create_compiled_grammars
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class XGrammarLogitsProcessor(transformers.LogitsProcessor):
|
|
36
|
+
"""
|
|
37
|
+
Inspired by [LogitsProcessor](https://github.com/mlc-ai/xgrammar/blob/414473e7c029d0d9e2dfbeacb48afa946d0e3419/python/xgrammar/contrib/hf.py#L14).
|
|
38
|
+
HuggingFace's XGrammarLogitsProcessor cannot be reused. Logits processors must be initialized for each call to generate().
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, compiled_grammars: list[xgr.CompiledGrammar]):
|
|
42
|
+
self.compiled_grammars = compiled_grammars
|
|
43
|
+
self.vocab_size = self.compiled_grammars[0].tokenizer_info.vocab_size
|
|
44
|
+
self.batch_size = len(compiled_grammars)
|
|
45
|
+
|
|
46
|
+
self.matchers: list[xgr.GrammarMatcher] | None = None
|
|
47
|
+
self.token_bitmask: torch.Tensor | None = None
|
|
48
|
+
self.prefilled = False
|
|
49
|
+
|
|
50
|
+
def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor) -> torch.FloatTensor:
|
|
51
|
+
# lazily initialize GrammarMatchers and bitmask
|
|
52
|
+
if self.matchers is None:
|
|
53
|
+
self.matchers = [xgr.GrammarMatcher(self.compiled_grammars[i]) for i in range(self.batch_size)]
|
|
54
|
+
self.token_bitmask = xgr.allocate_token_bitmask(self.batch_size, self.vocab_size)
|
|
55
|
+
|
|
56
|
+
if input_ids.shape[0] != self.batch_size:
|
|
57
|
+
raise RuntimeError(
|
|
58
|
+
"Expect input_ids.shape[0] to be XGrammarLogitsProcessor.batch_size. "
|
|
59
|
+
+ f"Got {input_ids.shape[0]} for the former, and {self.batch_size} for the latter."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if not self.prefilled:
|
|
63
|
+
# have not sampled a token yet
|
|
64
|
+
self.prefilled = True
|
|
65
|
+
else:
|
|
66
|
+
for i in range(self.batch_size):
|
|
67
|
+
if not self.matchers[i].is_terminated():
|
|
68
|
+
sampled_token = input_ids[i][-1]
|
|
69
|
+
assert self.matchers[i].accept_token(sampled_token)
|
|
28
70
|
|
|
29
|
-
|
|
30
|
-
|
|
71
|
+
for i in range(self.batch_size):
|
|
72
|
+
if not self.matchers[i].is_terminated():
|
|
73
|
+
self.matchers[i].fill_next_token_bitmask(self.token_bitmask, i)
|
|
74
|
+
|
|
75
|
+
device_type = scores.device.type
|
|
76
|
+
if device_type != "cuda":
|
|
77
|
+
scores = scores.to("cpu")
|
|
78
|
+
xgr.apply_token_bitmask_inplace(scores, self.token_bitmask.to(scores.device))
|
|
79
|
+
if device_type != "cuda":
|
|
80
|
+
scores = scores.to(device_type)
|
|
81
|
+
return scores
|
|
31
82
|
|
|
32
83
|
|
|
33
84
|
class HuggingFaceEngine(LanguageEngine):
|
|
@@ -37,13 +88,13 @@ class HuggingFaceEngine(LanguageEngine):
|
|
|
37
88
|
self.device = device
|
|
38
89
|
self.max_new_tokens = max_new_tokens
|
|
39
90
|
self.tokenizer_max_length = tokenizer_max_length
|
|
91
|
+
self.is_peft_adapter = (Path(model_path) / "adapter_config.json").exists()
|
|
40
92
|
|
|
41
|
-
is_peft_adapter = (Path(model_path) / "adapter_config.json").exists()
|
|
42
93
|
model_path = str(model_path)
|
|
43
|
-
self._model,
|
|
44
|
-
model_path, device=device, is_peft_adapter=is_peft_adapter, is_training=False
|
|
94
|
+
self._model, self._model_config = load_base_model_and_config(
|
|
95
|
+
model_path, device=device, is_peft_adapter=self.is_peft_adapter, is_training=False
|
|
45
96
|
)
|
|
46
|
-
if is_peft_adapter:
|
|
97
|
+
if self.is_peft_adapter:
|
|
47
98
|
self._model = PeftModel.from_pretrained(self._model, model_path, is_trainable=False)
|
|
48
99
|
self._model = self._model.merge_and_unload()
|
|
49
100
|
self._default_batch_size = 64
|
|
@@ -62,12 +113,10 @@ class HuggingFaceEngine(LanguageEngine):
|
|
|
62
113
|
)
|
|
63
114
|
|
|
64
115
|
# we can't enforce JSON output if LSTM tokenizer training was skipped
|
|
65
|
-
is_trained_lstm_tokenizer = not is_peft_adapter and self.tokenizer.vocab_size > len(
|
|
116
|
+
is_trained_lstm_tokenizer = not self.is_peft_adapter and self.tokenizer.vocab_size > len(
|
|
66
117
|
self.tokenizer.special_tokens_map
|
|
67
118
|
)
|
|
68
|
-
self._json_enforcing_possible = is_peft_adapter or is_trained_lstm_tokenizer
|
|
69
|
-
if self.supports_json_enforcing():
|
|
70
|
-
monkey_patch_formatron()
|
|
119
|
+
self._json_enforcing_possible = self.is_peft_adapter or is_trained_lstm_tokenizer
|
|
71
120
|
self._logits_processors = None
|
|
72
121
|
|
|
73
122
|
def get_default_batch_size(self) -> int:
|
|
@@ -76,12 +125,14 @@ class HuggingFaceEngine(LanguageEngine):
|
|
|
76
125
|
def supports_json_enforcing(self) -> bool:
|
|
77
126
|
return self._json_enforcing_possible
|
|
78
127
|
|
|
79
|
-
def initialize_logits_processors(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
128
|
+
def initialize_logits_processors(self, schemas: Generator[BaseModel]):
|
|
129
|
+
compiled_grammars = create_compiled_grammars(
|
|
130
|
+
schemas=schemas,
|
|
131
|
+
tokenizer=self.tokenizer,
|
|
132
|
+
vocab_size=self._model_config.vocab_size,
|
|
133
|
+
is_peft_adapter=self.is_peft_adapter,
|
|
84
134
|
)
|
|
135
|
+
self._logits_processors = [XGrammarLogitsProcessor(list(compiled_grammars))]
|
|
85
136
|
|
|
86
137
|
def generate(
|
|
87
138
|
self, text: list[str], sampling_temperature: float, sampling_top_p: float
|
|
@@ -107,22 +158,14 @@ class HuggingFaceEngine(LanguageEngine):
|
|
|
107
158
|
temperature=sampling_temperature if do_sample else None,
|
|
108
159
|
top_p=sampling_top_p if do_sample else None,
|
|
109
160
|
bos_token_id=self.tokenizer.bos_token_id,
|
|
110
|
-
pad_token_id=self.tokenizer.
|
|
161
|
+
pad_token_id=self.tokenizer.pad_token_id,
|
|
111
162
|
eos_token_id=self.tokenizer.eos_token_id,
|
|
112
163
|
)
|
|
113
164
|
|
|
114
|
-
if self._logits_processors is not None:
|
|
115
|
-
# number of formatters must match the batch size, batch size is always reduced so this is fine
|
|
116
|
-
actual_batch_size = len(inputs["input_ids"])
|
|
117
|
-
self._logits_processors[0]._formatters = self._logits_processors[0]._formatters[:actual_batch_size]
|
|
118
|
-
|
|
119
165
|
t_generate = time.time()
|
|
120
166
|
outputs = self._model.generate(**inputs, **generate_kwargs, logits_processor=self._logits_processors)
|
|
121
167
|
generate_time = time.time() - t_generate
|
|
122
168
|
|
|
123
|
-
if self._logits_processors:
|
|
124
|
-
self._logits_processors[0].reset()
|
|
125
|
-
|
|
126
169
|
_, input_length = inputs["input_ids"].shape
|
|
127
170
|
# truncate the prompt from the outputs
|
|
128
171
|
outputs = outputs[:, input_length:]
|
{mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_language/engine/vllm_engine.py
RENAMED
|
@@ -12,31 +12,30 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
15
17
|
import contextlib
|
|
16
|
-
|
|
18
|
+
import gc
|
|
17
19
|
import time
|
|
18
|
-
import
|
|
20
|
+
from collections.abc import Generator
|
|
21
|
+
from os import PathLike
|
|
19
22
|
|
|
20
|
-
from mostlyai.engine._language.common import is_bf16_supported
|
|
21
|
-
from mostlyai.engine._language.engine.base import EngineMetrics, LanguageEngine
|
|
22
23
|
import torch
|
|
23
|
-
|
|
24
|
-
from formatron.formatter import FormatterBuilder
|
|
25
|
-
|
|
24
|
+
import xgrammar as xgr
|
|
26
25
|
from peft import PeftConfig
|
|
27
|
-
|
|
28
|
-
from transformers import
|
|
29
|
-
from mostlyai.engine._language.formatron_utils import monkey_patch_formatron
|
|
26
|
+
from pydantic import BaseModel
|
|
27
|
+
from transformers import AutoConfig, AutoTokenizer
|
|
30
28
|
from vllm import LLM, SamplingParams
|
|
31
|
-
from vllm.lora.request import LoRARequest
|
|
32
29
|
from vllm.config import _get_and_verify_max_len
|
|
33
|
-
from
|
|
34
|
-
from
|
|
35
|
-
from vllm.distributed import destroy_model_parallel, destroy_distributed_environment
|
|
36
|
-
|
|
37
|
-
import gc
|
|
30
|
+
from vllm.distributed import destroy_distributed_environment, destroy_model_parallel
|
|
31
|
+
from vllm.lora.request import LoRARequest
|
|
38
32
|
from vllm.platforms import current_platform
|
|
39
33
|
|
|
34
|
+
from mostlyai.engine._language.common import is_bf16_supported
|
|
35
|
+
from mostlyai.engine._language.engine.base import EngineMetrics, LanguageEngine
|
|
36
|
+
from mostlyai.engine._language.tokenizer_utils import tokenize_fn
|
|
37
|
+
from mostlyai.engine._language.xgrammar_utils import create_compiled_grammars
|
|
38
|
+
|
|
40
39
|
|
|
41
40
|
def cleanup_dist_env_and_memory():
|
|
42
41
|
"""Copy from current main of vllm replace by import when possible"""
|
|
@@ -49,42 +48,76 @@ def cleanup_dist_env_and_memory():
|
|
|
49
48
|
torch.cuda.empty_cache()
|
|
50
49
|
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
llm: LLM,
|
|
54
|
-
formatter_builders: typing.Sequence[FormatterBuilder | None],
|
|
55
|
-
configs: typing.Sequence[EngineGenerationConfig] = None,
|
|
56
|
-
vocab_processors: list[typing.Callable] | None = None,
|
|
57
|
-
) -> list[FormattersLogitsProcessor]:
|
|
51
|
+
class XGrammarLogitsProcessor:
|
|
58
52
|
"""
|
|
59
|
-
|
|
53
|
+
Inspired by [XGrammarLogitsProcessor](https://github.com/vllm-project/vllm/blob/a43aa183dc0cb2639044c15d272e0ce1941392b0/vllm/model_executor/guided_decoding/xgrammar_decoding.py#L280).
|
|
54
|
+
VLLM's XGrammarLogitsProcessor can be reused.
|
|
60
55
|
"""
|
|
61
|
-
tokenizer = llm.get_tokenizer()
|
|
62
|
-
vocab = create_engine_vocabulary(tokenizer, vocab_processors)
|
|
63
|
-
formatters = [
|
|
64
|
-
i.build(vocab, lambda tokens: tokenizer.decode(tokens)) if i is not None else None for i in formatter_builders
|
|
65
|
-
]
|
|
66
|
-
return [FormattersLogitsProcessor([formatter], tokenizer.eos_token_id, configs) for formatter in formatters]
|
|
67
56
|
|
|
57
|
+
def __init__(self, compiled_grammar: xgr.CompiledGrammar):
|
|
58
|
+
self.compiled_grammar = compiled_grammar
|
|
59
|
+
self.tokenizer_info = compiled_grammar.tokenizer_info
|
|
60
|
+
self.batch_size = 1
|
|
68
61
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
This logits processor masks the output indices that do not correspond
|
|
73
|
-
to a token id.
|
|
74
|
-
"""
|
|
62
|
+
self.matchers: list[xgr.GrammarMatcher] | None = None
|
|
63
|
+
self.token_bitmask: torch.Tensor | None = None
|
|
64
|
+
self.prefilled = False
|
|
75
65
|
|
|
76
|
-
def
|
|
77
|
-
|
|
78
|
-
self.
|
|
66
|
+
def __call__(self, input_ids: tuple[int], scores: torch.Tensor) -> torch.Tensor:
|
|
67
|
+
# lazily initialize GrammarMatchers and bitmask
|
|
68
|
+
if self.matchers is None:
|
|
69
|
+
self.matchers = [xgr.GrammarMatcher(self.compiled_grammar) for _ in range(self.batch_size)]
|
|
70
|
+
self.token_bitmask = xgr.allocate_token_bitmask(self.batch_size, self.tokenizer_info.vocab_size)
|
|
79
71
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
72
|
+
if not self.prefilled:
|
|
73
|
+
# have not sampled a token yet
|
|
74
|
+
self.prefilled = True
|
|
75
|
+
else:
|
|
76
|
+
for i, matcher in enumerate(self.matchers):
|
|
77
|
+
if not matcher.is_terminated():
|
|
78
|
+
sampled_token = input_ids[-1]
|
|
79
|
+
assert self.matchers[i].accept_token(sampled_token)
|
|
80
|
+
|
|
81
|
+
for i, matcher in enumerate(self.matchers):
|
|
82
|
+
if not matcher.is_terminated():
|
|
83
|
+
matcher.fill_next_token_bitmask(self.token_bitmask, i)
|
|
84
|
+
|
|
85
|
+
# token_bitmask is a CPU tensor for use with accept_token and
|
|
86
|
+
# fill_next_token_bitmask so we move it to the device of scores
|
|
87
|
+
device_type = scores.device.type
|
|
88
|
+
dtype = scores.dtype
|
|
89
|
+
if device_type != "cuda":
|
|
90
|
+
# xgrammar on cpu only supports float32 scores
|
|
91
|
+
# see: https://github.com/mlc-ai/xgrammar/blob/c1b64920cad24f44f235778c1c00bb52d57da01a/python/xgrammar/kernels/apply_token_bitmask_inplace_cpu.py#L22
|
|
92
|
+
scores = scores.to("cpu").float().unsqueeze(0)
|
|
93
|
+
|
|
94
|
+
# Note: In this method, if the tensors have different dimensions
|
|
95
|
+
# on CPU device fails, but on GPU it runs without error. Hence the
|
|
96
|
+
# unsqueeze above for scores, to match the token bitmask shape
|
|
97
|
+
xgr.apply_token_bitmask_inplace(scores, self.token_bitmask.to(scores.device, non_blocking=True))
|
|
98
|
+
if device_type != "cuda":
|
|
99
|
+
scores = scores.to(dtype).to(device_type).squeeze()
|
|
86
100
|
return scores
|
|
87
101
|
|
|
102
|
+
def clone(self) -> XGrammarLogitsProcessor:
|
|
103
|
+
"""Create a new instance with shared compiled grammar but separate state"""
|
|
104
|
+
new_processor = XGrammarLogitsProcessor(self.compiled_grammar)
|
|
105
|
+
|
|
106
|
+
# create fresh matchers for the new sequence
|
|
107
|
+
if self.matchers is not None:
|
|
108
|
+
new_processor.matchers = [xgr.GrammarMatcher(self.compiled_grammar) for _ in range(self.batch_size)]
|
|
109
|
+
|
|
110
|
+
# create a new token bitmask with the same size
|
|
111
|
+
if self.token_bitmask is not None:
|
|
112
|
+
new_processor.token_bitmask = self.token_bitmask
|
|
113
|
+
|
|
114
|
+
new_processor.batch_size = self.batch_size
|
|
115
|
+
|
|
116
|
+
# reset prefilled state for new sequence
|
|
117
|
+
new_processor.prefilled = False
|
|
118
|
+
|
|
119
|
+
return new_processor
|
|
120
|
+
|
|
88
121
|
|
|
89
122
|
class VLLMEngine(LanguageEngine):
|
|
90
123
|
def __init__(
|
|
@@ -113,7 +146,6 @@ class VLLMEngine(LanguageEngine):
|
|
|
113
146
|
swap_space=0,
|
|
114
147
|
disable_log_stats=True,
|
|
115
148
|
)
|
|
116
|
-
self._base_logits_processors = [MaskInvalidIndicesLogitsProcessor(self.llm.get_tokenizer())]
|
|
117
149
|
self.tokenizer = AutoTokenizer.from_pretrained(
|
|
118
150
|
model_path,
|
|
119
151
|
padding_side="left",
|
|
@@ -124,7 +156,6 @@ class VLLMEngine(LanguageEngine):
|
|
|
124
156
|
add_eos_token=False,
|
|
125
157
|
)
|
|
126
158
|
self._logits_processors = None
|
|
127
|
-
monkey_patch_formatron()
|
|
128
159
|
|
|
129
160
|
def get_default_batch_size(self) -> int:
|
|
130
161
|
return 192
|
|
@@ -132,12 +163,14 @@ class VLLMEngine(LanguageEngine):
|
|
|
132
163
|
def supports_json_enforcing(self) -> bool:
|
|
133
164
|
return True
|
|
134
165
|
|
|
135
|
-
def initialize_logits_processors(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
166
|
+
def initialize_logits_processors(self, schemas: Generator[BaseModel]):
|
|
167
|
+
compiled_grammars = create_compiled_grammars(
|
|
168
|
+
schemas=schemas,
|
|
169
|
+
tokenizer=self.llm.get_tokenizer(),
|
|
170
|
+
vocab_size=self.llm.llm_engine.get_model_config().get_vocab_size(),
|
|
171
|
+
is_peft_adapter=True,
|
|
140
172
|
)
|
|
173
|
+
self._logits_processors = [XGrammarLogitsProcessor(compiled_grammar) for compiled_grammar in compiled_grammars]
|
|
141
174
|
|
|
142
175
|
def generate(
|
|
143
176
|
self, text: list[str], sampling_temperature: float, sampling_top_p: float
|
|
@@ -156,23 +189,15 @@ class VLLMEngine(LanguageEngine):
|
|
|
156
189
|
tokenize_time = time.time() - t_tokenize
|
|
157
190
|
|
|
158
191
|
actual_batch_size = len(inputs["input_ids"])
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
SamplingParams(
|
|
162
|
-
max_tokens=self.max_new_tokens,
|
|
163
|
-
temperature=sampling_temperature,
|
|
164
|
-
top_p=sampling_top_p,
|
|
165
|
-
logits_processors=[lp, *self._base_logits_processors],
|
|
166
|
-
)
|
|
167
|
-
for lp in self._logits_processors[:actual_batch_size]
|
|
168
|
-
]
|
|
169
|
-
else:
|
|
170
|
-
sampling_params = SamplingParams(
|
|
192
|
+
sampling_params = [
|
|
193
|
+
SamplingParams(
|
|
171
194
|
max_tokens=self.max_new_tokens,
|
|
172
195
|
temperature=sampling_temperature,
|
|
173
196
|
top_p=sampling_top_p,
|
|
174
|
-
logits_processors=
|
|
197
|
+
logits_processors=[lp],
|
|
175
198
|
)
|
|
199
|
+
for lp in self._logits_processors[:actual_batch_size]
|
|
200
|
+
]
|
|
176
201
|
t_generate = time.time()
|
|
177
202
|
outputs = self.llm.generate(
|
|
178
203
|
prompts=None,
|
|
@@ -182,9 +207,6 @@ class VLLMEngine(LanguageEngine):
|
|
|
182
207
|
lora_request=self._lora_request,
|
|
183
208
|
)
|
|
184
209
|
generate_time = time.time() - t_generate
|
|
185
|
-
if self._logits_processors is not None:
|
|
186
|
-
for lp in self._logits_processors:
|
|
187
|
-
lp.reset()
|
|
188
210
|
metrics = EngineMetrics(tokenize_time=tokenize_time, generate_time=generate_time)
|
|
189
211
|
return [r.outputs[0].token_ids for r in outputs], metrics
|
|
190
212
|
|
|
@@ -40,12 +40,9 @@ from mostlyai.engine._encoding_types.language.numeric import decode_language_num
|
|
|
40
40
|
from mostlyai.engine._encoding_types.language.text import decode_text
|
|
41
41
|
from mostlyai.engine._language.common import MAX_LENGTH
|
|
42
42
|
from mostlyai.engine._language.encoding import encode_df
|
|
43
|
+
from mostlyai.engine._language.xgrammar_utils import create_schemas
|
|
43
44
|
from mostlyai.engine._workspace import ensure_workspace_dir, Workspace, reset_dir
|
|
44
|
-
from mostlyai.engine._language.
|
|
45
|
-
get_formatter_builders,
|
|
46
|
-
prepare_seed_for_formatron,
|
|
47
|
-
get_vocab_processors,
|
|
48
|
-
)
|
|
45
|
+
from mostlyai.engine._language.xgrammar_utils import ensure_seed_can_be_tokenized
|
|
49
46
|
from mostlyai.engine.domain import ModelEncodingType, RareCategoryReplacementMethod
|
|
50
47
|
|
|
51
48
|
INVALID_VALUE = "_INVALID_" # when JSON parsing fails, the values of target columns will be set to this
|
|
@@ -274,8 +271,7 @@ def generate(
|
|
|
274
271
|
batch_size = sample_size
|
|
275
272
|
_LOG.info(f"{batch_size=}")
|
|
276
273
|
|
|
277
|
-
|
|
278
|
-
seed_data = prepare_seed_for_formatron(seed_data, engine.tokenizer)
|
|
274
|
+
seed_data = ensure_seed_can_be_tokenized(seed_data, engine.tokenizer)
|
|
279
275
|
seeded_tgt_columns = seed_data.columns.to_list()
|
|
280
276
|
|
|
281
277
|
total_tokenize_fn_time = 0
|
|
@@ -284,14 +280,16 @@ def generate(
|
|
|
284
280
|
|
|
285
281
|
enforce_json_output = engine.supports_json_enforcing()
|
|
286
282
|
_LOG.info(f"{enforce_json_output=}")
|
|
287
|
-
formatron_vocab_processors = get_vocab_processors(is_peft_adapter)
|
|
288
283
|
|
|
289
|
-
|
|
284
|
+
initialize_logits_processors_once = len(seeded_tgt_columns) == 0 and engine.__class__.__name__ == "VLLMEngine"
|
|
285
|
+
if enforce_json_output and initialize_logits_processors_once:
|
|
290
286
|
t0 = time.time()
|
|
291
|
-
|
|
292
|
-
size=batch_size,
|
|
287
|
+
schemas = create_schemas(
|
|
288
|
+
size=batch_size,
|
|
289
|
+
stats=tgt_stats,
|
|
290
|
+
rare_category_replacement_method=rare_category_replacement_method,
|
|
293
291
|
)
|
|
294
|
-
engine.initialize_logits_processors(
|
|
292
|
+
engine.initialize_logits_processors(schemas=schemas)
|
|
295
293
|
total_logits_processor_build_time += time.time() - t0
|
|
296
294
|
|
|
297
295
|
# keep at most 500k samples in memory before decoding and writing to disk
|
|
@@ -305,15 +303,14 @@ def generate(
|
|
|
305
303
|
ctx_batch = ctx_data.iloc[samples_processed : samples_processed + batch_size]
|
|
306
304
|
ctx_keys = ctx_batch[ctx_primary_key]
|
|
307
305
|
|
|
308
|
-
if enforce_json_output and
|
|
306
|
+
if enforce_json_output and not initialize_logits_processors_once:
|
|
309
307
|
t0 = time.time()
|
|
310
|
-
|
|
311
|
-
formatter_builders = get_formatter_builders(
|
|
308
|
+
schemas = create_schemas(
|
|
312
309
|
seed_df=sample_seed_batch,
|
|
313
310
|
stats=tgt_stats,
|
|
314
311
|
rare_category_replacement_method=rare_category_replacement_method,
|
|
315
312
|
)
|
|
316
|
-
engine.initialize_logits_processors(
|
|
313
|
+
engine.initialize_logits_processors(schemas=schemas)
|
|
317
314
|
total_logits_processor_build_time += time.time() - t0
|
|
318
315
|
|
|
319
316
|
outputs, metrics = engine.generate(
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Copyright 2025 MOSTLY AI
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from collections.abc import Generator
|
|
19
|
+
from typing import Literal
|
|
20
|
+
|
|
21
|
+
import pandas as pd
|
|
22
|
+
import xgrammar as xgr
|
|
23
|
+
from pydantic import BaseModel, Field, SkipValidation, create_model
|
|
24
|
+
from transformers import PreTrainedTokenizerBase
|
|
25
|
+
from xgrammar.testing import _json_schema_to_ebnf
|
|
26
|
+
|
|
27
|
+
from mostlyai.engine._common import STRING
|
|
28
|
+
from mostlyai.engine._encoding_types.language.categorical import CATEGORICAL_UNKNOWN_TOKEN
|
|
29
|
+
from mostlyai.engine.domain import ModelEncodingType, RareCategoryReplacementMethod
|
|
30
|
+
|
|
31
|
+
JSON_NULL = "null"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def prepend_grammar_root_with_space(grammar: str) -> str:
|
|
35
|
+
# XGrammar always starts with "{" when enforcing JSON Schema
|
|
36
|
+
# training is done on strings like ` {...} {...}`
|
|
37
|
+
# later, generation is done with prompt like ` {...}`
|
|
38
|
+
# so the first output token must be a space
|
|
39
|
+
start_of_grammar = 'root ::= "{"'
|
|
40
|
+
start_of_grammar_with_space = 'root ::= " {"'
|
|
41
|
+
assert start_of_grammar in grammar
|
|
42
|
+
return grammar.replace(start_of_grammar, start_of_grammar_with_space)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def ensure_seed_can_be_tokenized(sample_seed: pd.DataFrame, tokenizer: PreTrainedTokenizerBase) -> pd.DataFrame:
|
|
46
|
+
def transform(x: str | pd._libs.missing.NAType) -> str:
|
|
47
|
+
if pd.isna(x):
|
|
48
|
+
null = tokenizer.decode(tokenizer.encode(JSON_NULL), skip_special_tokens=True)
|
|
49
|
+
# xgrammar needs to be able to express JSON_NULL with available vocabulary
|
|
50
|
+
# if that's the case, harmonize null-like values to None (e.g. pd.NA would cause xgrammar to fail)
|
|
51
|
+
# otherwise, fallback to empty string
|
|
52
|
+
return None if null == JSON_NULL else ""
|
|
53
|
+
# skip tokens unseen during training
|
|
54
|
+
return tokenizer.decode(tokenizer.encode(x), skip_special_tokens=True)
|
|
55
|
+
|
|
56
|
+
return sample_seed.astype(STRING).map(transform)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def create_schemas(
|
|
60
|
+
*,
|
|
61
|
+
seed_df: pd.DataFrame | None = None,
|
|
62
|
+
size: int | None = None,
|
|
63
|
+
stats: dict,
|
|
64
|
+
rare_category_replacement_method: RareCategoryReplacementMethod,
|
|
65
|
+
) -> Generator[BaseModel]:
|
|
66
|
+
assert (seed_df is not None) ^ (size is not None), "exactly one of seed_df or size must be provided"
|
|
67
|
+
if seed_df is None:
|
|
68
|
+
seed_df = pd.DataFrame(index=range(size))
|
|
69
|
+
unseeded_fields = [c for c in list(stats["columns"].keys()) if c not in seed_df.columns.to_list()]
|
|
70
|
+
field_types = {
|
|
71
|
+
t: [col for col, col_stats in stats["columns"].items() if col_stats["encoding_type"] == t]
|
|
72
|
+
for t in ModelEncodingType
|
|
73
|
+
}
|
|
74
|
+
categorical_fields = field_types.get(ModelEncodingType.language_categorical, [])
|
|
75
|
+
numeric_fields = field_types.get(ModelEncodingType.language_numeric, [])
|
|
76
|
+
datetime_fields = field_types.get(ModelEncodingType.language_datetime, [])
|
|
77
|
+
cache = {}
|
|
78
|
+
for _, seed_row in seed_df.iterrows():
|
|
79
|
+
cache_key = hash(tuple(sorted([(field_name, str(seed_value)) for field_name, seed_value in seed_row.items()])))
|
|
80
|
+
if cache_key in cache:
|
|
81
|
+
yield cache[cache_key]
|
|
82
|
+
continue
|
|
83
|
+
model_dict = {}
|
|
84
|
+
if not seed_row.empty:
|
|
85
|
+
model_dict |= {field_name: (Literal[seed_value], ...) for field_name, seed_value in seed_row.items()} # type: ignore[valid-type]
|
|
86
|
+
for field_name in unseeded_fields:
|
|
87
|
+
if field_name in categorical_fields:
|
|
88
|
+
categories = stats["columns"][field_name]["categories"]
|
|
89
|
+
if rare_category_replacement_method == RareCategoryReplacementMethod.sample and len(categories) > 1:
|
|
90
|
+
categories = [c for c in categories if c != CATEGORICAL_UNKNOWN_TOKEN]
|
|
91
|
+
model_dict[field_name] = (Literal[tuple(categories)], ...) # type: ignore[valid-type]
|
|
92
|
+
elif field_name in numeric_fields:
|
|
93
|
+
max_scale = stats["columns"][field_name]["max_scale"]
|
|
94
|
+
min_min5 = min(stats["columns"][field_name]["min5"])
|
|
95
|
+
max_max5 = max(stats["columns"][field_name]["max5"])
|
|
96
|
+
if max_scale == 0:
|
|
97
|
+
model_dict[field_name] = (SkipValidation[int], Field(ge=min_min5, le=max_max5))
|
|
98
|
+
else:
|
|
99
|
+
model_dict[field_name] = (
|
|
100
|
+
SkipValidation[float],
|
|
101
|
+
Field(ge=min_min5, le=max_max5, decimal_places=max_scale),
|
|
102
|
+
)
|
|
103
|
+
elif field_name in datetime_fields:
|
|
104
|
+
model_dict[field_name] = (
|
|
105
|
+
SkipValidation[str],
|
|
106
|
+
Field(
|
|
107
|
+
pattern=r"""(19\d{2}|20\d{2})-(0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-9]|3[0-1])T([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])"""
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
else:
|
|
111
|
+
model_dict[field_name] = (str, ...)
|
|
112
|
+
schema = create_model("TargetModel", **model_dict)
|
|
113
|
+
cache[cache_key] = schema
|
|
114
|
+
yield schema
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _get_tokenizer_info_for_lstm(tokenizer: PreTrainedTokenizerBase, vocab_size: int):
|
|
118
|
+
# trimmed down version of xgr.TokenizerInfo.from_huggingface
|
|
119
|
+
# the original function sets vocab_type to VocabType.RAW,
|
|
120
|
+
# but LSTM tokenizer needs VocabType.BYTE_FALLBACK, because of the usage of metaspace ("▁")
|
|
121
|
+
encoded_vocab = [""] * vocab_size
|
|
122
|
+
for token, idx in tokenizer.get_vocab().items():
|
|
123
|
+
if idx < vocab_size:
|
|
124
|
+
encoded_vocab[idx] = token
|
|
125
|
+
tokenizer_info = xgr.TokenizerInfo(
|
|
126
|
+
encoded_vocab,
|
|
127
|
+
vocab_type=xgr.VocabType.BYTE_FALLBACK,
|
|
128
|
+
vocab_size=vocab_size,
|
|
129
|
+
stop_token_ids=[tokenizer.eos_token_id],
|
|
130
|
+
add_prefix_space=True,
|
|
131
|
+
)
|
|
132
|
+
return tokenizer_info
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def create_compiled_grammars(
|
|
136
|
+
schemas: Generator[BaseModel], tokenizer: PreTrainedTokenizerBase, vocab_size: int, is_peft_adapter: bool
|
|
137
|
+
) -> Generator[xgr.CompiledGrammar]:
|
|
138
|
+
# in general, there might be misalignment between the model's and tokenizer's vocab_size
|
|
139
|
+
# the former is expected by XGrammar
|
|
140
|
+
make_tokenizer_info = xgr.TokenizerInfo.from_huggingface if is_peft_adapter else _get_tokenizer_info_for_lstm
|
|
141
|
+
tokenizer_info = make_tokenizer_info(tokenizer, vocab_size=vocab_size)
|
|
142
|
+
grammar_compiler = xgr.GrammarCompiler(tokenizer_info)
|
|
143
|
+
schemas = (json.dumps(schema.model_json_schema()) for schema in schemas)
|
|
144
|
+
grammars = (_json_schema_to_ebnf(schema) for schema in schemas)
|
|
145
|
+
grammars = (prepend_grammar_root_with_space(grammar) for grammar in grammars)
|
|
146
|
+
compiled_grammars = (grammar_compiler.compile_grammar(grammar) for grammar in grammars)
|
|
147
|
+
return compiled_grammars
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mostlyai-engine"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.2.0"
|
|
4
4
|
description = "Synthetic Data Engine"
|
|
5
5
|
authors = [{ name = "MOSTLY AI", email = "dev@mostly.ai" }]
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -26,21 +26,20 @@ classifiers = [
|
|
|
26
26
|
|
|
27
27
|
dependencies = [
|
|
28
28
|
"setuptools>=75.0.0,<76",
|
|
29
|
-
"numpy>=1.26.3
|
|
29
|
+
"numpy>=1.26.3",
|
|
30
30
|
"pandas~=2.2.0",
|
|
31
31
|
"pyarrow>=16.0.0",
|
|
32
|
-
"joblib>=1.4.2
|
|
32
|
+
"joblib>=1.4.2",
|
|
33
33
|
"psutil>=5.9.5,<6", # upgrade when colab psutil is updated
|
|
34
|
-
"tokenizers>=0.21.0
|
|
35
|
-
"transformers>=4.
|
|
36
|
-
"datasets>=3.0.0
|
|
37
|
-
"accelerate>=
|
|
34
|
+
"tokenizers>=0.21.0",
|
|
35
|
+
"transformers>=4.51.0",
|
|
36
|
+
"datasets>=3.0.0",
|
|
37
|
+
"accelerate>=1.5.0",
|
|
38
38
|
"peft>=0.12.0",
|
|
39
|
-
"huggingface-hub>=0.
|
|
40
|
-
"opacus>=1.5.2
|
|
41
|
-
"
|
|
39
|
+
"huggingface-hub>=0.30.0",
|
|
40
|
+
"opacus>=1.5.2", # switch to 1.5.4 (once released) to allow numpy 2
|
|
41
|
+
"xgrammar==0.1.17", # for vllm 0.8.2 compatibility
|
|
42
42
|
"json-repair>=0.30.0",
|
|
43
|
-
"rich>=13.9.4,<14",
|
|
44
43
|
"torch>=2.6.0; sys_platform == 'darwin'",
|
|
45
44
|
"torch==2.6.0; sys_platform == 'linux'", # for vllm 0.8.2 compatibility
|
|
46
45
|
"torchaudio==2.6.0; sys_platform == 'linux'", # for vllm 0.8.2 compatibility
|
|
@@ -118,8 +117,11 @@ include = ["mostlyai/engine"]
|
|
|
118
117
|
[tool.hatch.build.targets.wheel]
|
|
119
118
|
include = ["mostlyai/engine"]
|
|
120
119
|
|
|
120
|
+
[tool.hatch.metadata]
|
|
121
|
+
allow-direct-references = true
|
|
122
|
+
|
|
121
123
|
[build-system]
|
|
122
|
-
requires = ["hatchling"]
|
|
124
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
123
125
|
build-backend = "hatchling.build"
|
|
124
126
|
|
|
125
127
|
[tool.ruff]
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
# Copyright 2025 MOSTLY AI
|
|
2
|
-
#
|
|
3
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
# you may not use this file except in compliance with the License.
|
|
5
|
-
# You may obtain a copy of the License at
|
|
6
|
-
#
|
|
7
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
#
|
|
9
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
# See the License for the specific language governing permissions and
|
|
13
|
-
# limitations under the License.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import typing
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
import pandas as pd
|
|
20
|
-
from formatron.schemas.pydantic import ClassSchema
|
|
21
|
-
from json import JSONDecodeError
|
|
22
|
-
from pydantic import Field, SkipValidation, ValidationError
|
|
23
|
-
from formatron.formatter import FormatterBuilder
|
|
24
|
-
from formatron import schemas
|
|
25
|
-
from formatron.formats import json
|
|
26
|
-
from typing import Literal
|
|
27
|
-
from pydantic import create_model
|
|
28
|
-
from transformers import PreTrainedTokenizerBase
|
|
29
|
-
from mostlyai.engine._encoding_types.language.categorical import CATEGORICAL_UNKNOWN_TOKEN
|
|
30
|
-
from mostlyai.engine.domain import ModelEncodingType, RareCategoryReplacementMethod
|
|
31
|
-
|
|
32
|
-
JSON_NULL = "null"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def prepare_seed_for_formatron(sample_seed: pd.DataFrame, tokenizer: PreTrainedTokenizerBase) -> pd.DataFrame:
|
|
36
|
-
def transform(x: str | None) -> str:
|
|
37
|
-
if pd.isna(x):
|
|
38
|
-
null = tokenizer.decode(tokenizer.encode(JSON_NULL), skip_special_tokens=True)
|
|
39
|
-
# formatron needs to be able to express JSON_NULL with available vocabulary
|
|
40
|
-
# if that's the case, harmonize null-like values to None (e.g. pd.NA would cause formatron to fail)
|
|
41
|
-
# otherwise, fallback to empty string
|
|
42
|
-
return None if null == JSON_NULL else ""
|
|
43
|
-
# skip tokens unseen during training
|
|
44
|
-
return tokenizer.decode(tokenizer.encode(x), skip_special_tokens=True)
|
|
45
|
-
|
|
46
|
-
return sample_seed.astype("string[pyarrow]").map(transform)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def get_formatter_builders(
|
|
50
|
-
*,
|
|
51
|
-
seed_df: pd.DataFrame | None = None,
|
|
52
|
-
size: int | None = None,
|
|
53
|
-
stats: dict,
|
|
54
|
-
rare_category_replacement_method: RareCategoryReplacementMethod,
|
|
55
|
-
) -> list[FormatterBuilder]:
|
|
56
|
-
assert (seed_df is not None) ^ (size is not None), "exactly one of seed_df or size must be provided"
|
|
57
|
-
formatter_builders = []
|
|
58
|
-
if seed_df is None:
|
|
59
|
-
seed_df = pd.DataFrame(index=range(size))
|
|
60
|
-
unseeded_fields = [c for c in list(stats["columns"].keys()) if c not in seed_df.columns.to_list()]
|
|
61
|
-
field_types = {
|
|
62
|
-
t: [col for col, col_stats in stats["columns"].items() if col_stats["encoding_type"] == t]
|
|
63
|
-
for t in ModelEncodingType
|
|
64
|
-
}
|
|
65
|
-
categorical_fields = field_types.get(ModelEncodingType.language_categorical, [])
|
|
66
|
-
numeric_fields = field_types.get(ModelEncodingType.language_numeric, [])
|
|
67
|
-
datetime_fields = field_types.get(ModelEncodingType.language_datetime, [])
|
|
68
|
-
cache = {}
|
|
69
|
-
for _, seed_row in seed_df.iterrows():
|
|
70
|
-
cache_key = hash(tuple(sorted([(field_name, str(seed_value)) for field_name, seed_value in seed_row.items()])))
|
|
71
|
-
if cache_key in cache:
|
|
72
|
-
formatter_builders.append(cache[cache_key])
|
|
73
|
-
continue
|
|
74
|
-
model_dict = {}
|
|
75
|
-
if not seed_row.empty:
|
|
76
|
-
model_dict |= {field_name: (Literal[seed_value], ...) for field_name, seed_value in seed_row.items()} # type: ignore[valid-type]
|
|
77
|
-
for field_name in unseeded_fields:
|
|
78
|
-
if field_name in categorical_fields:
|
|
79
|
-
categories = stats["columns"][field_name]["categories"]
|
|
80
|
-
if rare_category_replacement_method == RareCategoryReplacementMethod.sample and len(categories) > 1:
|
|
81
|
-
categories = [c for c in categories if c != CATEGORICAL_UNKNOWN_TOKEN]
|
|
82
|
-
model_dict[field_name] = (Literal[tuple(categories)], ...) # type: ignore[valid-type]
|
|
83
|
-
elif field_name in numeric_fields:
|
|
84
|
-
max_scale = stats["columns"][field_name]["max_scale"]
|
|
85
|
-
min_min5 = min(stats["columns"][field_name]["min5"])
|
|
86
|
-
max_max5 = max(stats["columns"][field_name]["max5"])
|
|
87
|
-
if max_scale == 0:
|
|
88
|
-
model_dict[field_name] = (SkipValidation[int], Field(ge=min_min5, le=max_max5))
|
|
89
|
-
else:
|
|
90
|
-
model_dict[field_name] = (
|
|
91
|
-
SkipValidation[float],
|
|
92
|
-
Field(ge=min_min5, le=max_max5, decimal_places=max_scale),
|
|
93
|
-
)
|
|
94
|
-
elif field_name in datetime_fields:
|
|
95
|
-
model_dict[field_name] = (
|
|
96
|
-
SkipValidation[str],
|
|
97
|
-
Field(
|
|
98
|
-
pattern=r"""(19\\d{2}|20\\d{2})-(0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-9]|3[0-1])T([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])"""
|
|
99
|
-
),
|
|
100
|
-
)
|
|
101
|
-
else:
|
|
102
|
-
model_dict[field_name] = (str, ...)
|
|
103
|
-
schema = create_model("TargetModel", **model_dict, __base__=MostlyClassSchema)
|
|
104
|
-
formatter_builder = FormatterBuilder()
|
|
105
|
-
formatter_builder.append_str(f"{formatter_builder.json(schema, capture_name=None)}")
|
|
106
|
-
cache[cache_key] = formatter_builder
|
|
107
|
-
formatter_builders.append(formatter_builder)
|
|
108
|
-
return formatter_builders
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def get_vocab_processors(is_peft_adapter: bool) -> list[typing.Callable] | None:
|
|
112
|
-
if not is_peft_adapter:
|
|
113
|
-
|
|
114
|
-
def update_vocab_lstm(token_to_char: dict[bytes, bytes]):
|
|
115
|
-
"""
|
|
116
|
-
Maps special tokens ("▁", "␊") back to their original representation (" ", "\n")
|
|
117
|
-
(used in LSTM tokenizer)
|
|
118
|
-
"""
|
|
119
|
-
token_to_char["\u2581".encode()] = b" " # "▁" -> " "
|
|
120
|
-
token_to_char["\u240a".encode()] = b"\n" # "␊" -> "\n"
|
|
121
|
-
|
|
122
|
-
return [update_vocab_lstm]
|
|
123
|
-
return None
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
class MostlyClassSchema(ClassSchema):
|
|
127
|
-
@classmethod
|
|
128
|
-
def from_json(cls, _json: str) -> "MostlyClassSchema":
|
|
129
|
-
"""
|
|
130
|
-
Create a MostlyClassSchema from a JSON string.
|
|
131
|
-
"""
|
|
132
|
-
try:
|
|
133
|
-
return cls.model_validate_json(_json)
|
|
134
|
-
except ValidationError as e:
|
|
135
|
-
for error in e.errors():
|
|
136
|
-
if error["type"] == "json_invalid":
|
|
137
|
-
raise JSONDecodeError(
|
|
138
|
-
f"Caught pydantic ValidationError {e}, reraising as JSONDecodeError", _json, 0
|
|
139
|
-
)
|
|
140
|
-
raise e
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
# copy formatron: direct copy from formatron
|
|
144
|
-
def _string_metadata(current: type, nonterminal: str):
|
|
145
|
-
min_length = current.metadata.get("min_length")
|
|
146
|
-
max_length = current.metadata.get("max_length")
|
|
147
|
-
pattern = current.metadata.get("pattern")
|
|
148
|
-
substring_of = current.metadata.get("substring_of")
|
|
149
|
-
if pattern:
|
|
150
|
-
assert not (min_length or max_length or substring_of), (
|
|
151
|
-
"pattern is mutually exclusive with min_length, max_length and substring_of"
|
|
152
|
-
)
|
|
153
|
-
if substring_of:
|
|
154
|
-
assert not (min_length or max_length or pattern), (
|
|
155
|
-
"substring_of is mutually exclusive with min_length, max_length and pattern"
|
|
156
|
-
)
|
|
157
|
-
repetition_map = {
|
|
158
|
-
(True, False): f"{{{min_length},}}",
|
|
159
|
-
(False, True): f"{{0,{max_length}}}",
|
|
160
|
-
(True, True): f"{{{min_length},{max_length}}}",
|
|
161
|
-
}
|
|
162
|
-
repetition = repetition_map.get((min_length is not None, max_length is not None))
|
|
163
|
-
if repetition is not None:
|
|
164
|
-
return (
|
|
165
|
-
rf"""{nonterminal} ::= #'"([^\\\\"\u0000-\u001f]|\\\\["\\\\bfnrt/]|\\\\u[0-9A-Fa-f]{{4}}){repetition}"';
|
|
166
|
-
""",
|
|
167
|
-
[],
|
|
168
|
-
)
|
|
169
|
-
if pattern is not None:
|
|
170
|
-
pattern = pattern.replace("'", "\\'")
|
|
171
|
-
return f"""{nonterminal} ::= #'"{pattern}"';\n""", []
|
|
172
|
-
if substring_of is not None:
|
|
173
|
-
return f"""{nonterminal} ::= '"' #substrs{repr(substring_of)} '"';\n""", []
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
# completely altered vs formatron
|
|
177
|
-
def _number_metadata(current: type, nonterminal: str):
|
|
178
|
-
# For now only constrains number of digits and whether it is negative
|
|
179
|
-
gt = current.metadata.get("gt")
|
|
180
|
-
ge = current.metadata.get("ge")
|
|
181
|
-
lt = current.metadata.get("lt")
|
|
182
|
-
le = current.metadata.get("le")
|
|
183
|
-
if lt is not None or gt is not None:
|
|
184
|
-
raise NotImplementedError("gt and lt are not supported for number metadata")
|
|
185
|
-
if le < ge:
|
|
186
|
-
raise ValueError("le must be greater than or equal to ge")
|
|
187
|
-
|
|
188
|
-
pattern_parts = []
|
|
189
|
-
if issubclass(current.type, float):
|
|
190
|
-
le, le_frac = str(le).split(".")
|
|
191
|
-
ge, ge_frac = str(ge).split(".")
|
|
192
|
-
le, le_frac = int(le), int(le_frac)
|
|
193
|
-
ge, ge_frac = int(ge), int(ge_frac)
|
|
194
|
-
decimal_places = current.metadata.get("decimal_places")
|
|
195
|
-
|
|
196
|
-
if ge is not None and le is not None:
|
|
197
|
-
if ge < 0 and le < 0:
|
|
198
|
-
pattern_parts.append("-")
|
|
199
|
-
min_num = abs(le)
|
|
200
|
-
max_num = abs(ge)
|
|
201
|
-
max_digits = len(str(max_num))
|
|
202
|
-
min_digits = len(str(min_num))
|
|
203
|
-
pattern_parts.append(rf"([1-9][0-9]{{{min_digits - 1},{max_digits - 1}}})")
|
|
204
|
-
elif ge > 0:
|
|
205
|
-
min_num = ge
|
|
206
|
-
max_num = le
|
|
207
|
-
max_digits = len(str(max_num))
|
|
208
|
-
min_digits = len(str(min_num))
|
|
209
|
-
pattern_parts.append(rf"([1-9][0-9]{{{min_digits - 1},{max_digits - 1}}})")
|
|
210
|
-
else:
|
|
211
|
-
if ge < 0:
|
|
212
|
-
pattern_parts.append("-?")
|
|
213
|
-
max_digits = max(len(str(abs(ge))), len(str(abs(le))))
|
|
214
|
-
pattern_parts.append(rf"(0|[1-9][0-9]{{0,{max_digits - 1}}})")
|
|
215
|
-
|
|
216
|
-
if issubclass(current.type, float):
|
|
217
|
-
pattern_parts.append(rf"(\\.[0-9]{{0,{decimal_places}}})?")
|
|
218
|
-
|
|
219
|
-
pattern = "".join(pattern_parts)
|
|
220
|
-
return f"""{nonterminal} ::= #"{pattern}";\n""", []
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
# copy formatron: removed sequence metadata since unnecessary and altered number_metadata to use ours
|
|
224
|
-
def _metadata(current: type, nonterminal: str):
|
|
225
|
-
if isinstance(current, schemas.schema.TypeWithMetadata):
|
|
226
|
-
original = typing.get_origin(current.type)
|
|
227
|
-
if original is None:
|
|
228
|
-
original = current.type
|
|
229
|
-
if not current.metadata:
|
|
230
|
-
return "", [(current.type, nonterminal)]
|
|
231
|
-
if isinstance(current.type, type) and issubclass(current.type, str):
|
|
232
|
-
return _string_metadata(current, nonterminal)
|
|
233
|
-
elif isinstance(current.type, type) and issubclass(current.type, (int, float)):
|
|
234
|
-
return _number_metadata(current, nonterminal)
|
|
235
|
-
return None
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def monkey_patch_formatron():
|
|
239
|
-
FORMATRON_WHITESPACE_MAX_REPETITIONS = 10
|
|
240
|
-
SPACE_NONTERMINAL = f"[ \t\n\r]{{0,{FORMATRON_WHITESPACE_MAX_REPETITIONS}}}"
|
|
241
|
-
|
|
242
|
-
# Copy from formatron, altered to have limited whitespace repetitions and datetime format
|
|
243
|
-
json.GRAMMAR_HEADER = rf"""integer ::= #"-?(0|[1-9]\\d*)";
|
|
244
|
-
number ::= #"-?(0|[1-9]\\d*)(\\.\\d+)?([eE][+-]?\\d+)?";
|
|
245
|
-
string ::= #'"([^\\\\"\u0000-\u001f]|\\\\["\\\\bfnrt/]|\\\\u[0-9A-Fa-f]{{4}})*"';
|
|
246
|
-
boolean ::= "true"|"false";
|
|
247
|
-
null ::= "null";
|
|
248
|
-
array ::= array_begin (json_value (comma json_value)*)? array_end;
|
|
249
|
-
object ::= object_begin (string colon json_value (comma string colon json_value)*)? object_end;
|
|
250
|
-
json_value ::= number|string|boolean|null|array|object;
|
|
251
|
-
comma ::= #"{SPACE_NONTERMINAL},{SPACE_NONTERMINAL}";
|
|
252
|
-
colon ::= #"{SPACE_NONTERMINAL}:{SPACE_NONTERMINAL}";
|
|
253
|
-
object_begin ::= #" \\{{{SPACE_NONTERMINAL}";
|
|
254
|
-
object_end ::= #"{SPACE_NONTERMINAL}\\}}";
|
|
255
|
-
array_begin ::= #"\\[{SPACE_NONTERMINAL}";
|
|
256
|
-
array_end ::= #"{SPACE_NONTERMINAL}\\]";
|
|
257
|
-
"""
|
|
258
|
-
|
|
259
|
-
def alter_type_to_nonterminals_metadata_inplace(type_to_nonterminals: list[typing.Callable]):
|
|
260
|
-
metadata_idx = [idx for idx, fn in enumerate(type_to_nonterminals) if fn.__name__ == "metadata"]
|
|
261
|
-
if len(metadata_idx) == 1:
|
|
262
|
-
type_to_nonterminals[metadata_idx[0]] = _metadata
|
|
263
|
-
|
|
264
|
-
alter_type_to_nonterminals_metadata_inplace(json._type_to_nonterminals)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/language/numeric.py
RENAMED
|
File without changes
|
{mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/language/text.py
RENAMED
|
File without changes
|
{mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/tabular/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/tabular/datetime.py
RENAMED
|
File without changes
|
{mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/tabular/itt.py
RENAMED
|
File without changes
|
{mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/tabular/lat_long.py
RENAMED
|
File without changes
|
{mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_encoding_types/tabular/numeric.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_language/engine/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyai_engine-1.1.12 → mostlyai_engine-1.2.0}/mostlyai/engine/_language/tokenizer_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|