mostlyai-engine 1.5.6__tar.gz → 1.5.7__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 (52) hide show
  1. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/PKG-INFO +7 -7
  2. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/__init__.py +1 -1
  3. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_language/engine/base.py +21 -5
  4. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_language/engine/hf_engine.py +17 -10
  5. mostlyai_engine-1.5.7/mostlyai/engine/_language/engine/vllm_engine.py +174 -0
  6. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_language/generation.py +14 -6
  7. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_language/lstm.py +5 -0
  8. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_language/training.py +3 -0
  9. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_tabular/training.py +3 -0
  10. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_training_utils.py +17 -0
  11. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/pyproject.toml +8 -8
  12. mostlyai_engine-1.5.6/mostlyai/engine/_language/engine/vllm_engine.py +0 -223
  13. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/.gitignore +0 -0
  14. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/LICENSE +0 -0
  15. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/README.md +0 -0
  16. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_common.py +0 -0
  17. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_dtypes.py +0 -0
  18. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_encoding_types/__init__.py +0 -0
  19. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_encoding_types/language/__init__.py +0 -0
  20. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_encoding_types/language/categorical.py +0 -0
  21. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_encoding_types/language/datetime.py +0 -0
  22. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_encoding_types/language/numeric.py +0 -0
  23. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_encoding_types/language/text.py +0 -0
  24. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_encoding_types/tabular/__init__.py +0 -0
  25. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_encoding_types/tabular/categorical.py +0 -0
  26. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_encoding_types/tabular/character.py +0 -0
  27. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_encoding_types/tabular/datetime.py +0 -0
  28. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_encoding_types/tabular/itt.py +0 -0
  29. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_encoding_types/tabular/lat_long.py +0 -0
  30. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_encoding_types/tabular/numeric.py +0 -0
  31. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_language/__init__.py +0 -0
  32. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_language/common.py +0 -0
  33. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_language/encoding.py +0 -0
  34. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_language/engine/__init__.py +0 -0
  35. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_language/tokenizer_utils.py +0 -0
  36. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_language/xgrammar_utils.py +0 -0
  37. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_memory.py +0 -0
  38. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_tabular/__init__.py +0 -0
  39. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_tabular/argn.py +0 -0
  40. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_tabular/common.py +0 -0
  41. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_tabular/encoding.py +0 -0
  42. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_tabular/fairness.py +0 -0
  43. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_tabular/generation.py +0 -0
  44. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/_workspace.py +0 -0
  45. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/analysis.py +0 -0
  46. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/domain.py +0 -0
  47. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/encoding.py +0 -0
  48. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/generation.py +0 -0
  49. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/logging.py +0 -0
  50. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/random_state.py +0 -0
  51. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/splitting.py +0 -0
  52. {mostlyai_engine-1.5.6 → mostlyai_engine-1.5.7}/mostlyai/engine/training.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mostlyai-engine
3
- Version: 1.5.6
3
+ Version: 1.5.7
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
@@ -37,15 +37,15 @@ Requires-Dist: psutil<6,>=5.9.5
37
37
  Requires-Dist: pyarrow>=16.0.0
38
38
  Requires-Dist: setuptools>=77.0.3
39
39
  Requires-Dist: tokenizers>=0.21.0
40
- Requires-Dist: torch<2.7.1,>=2.7.0
41
- Requires-Dist: torchaudio<2.7.1,>=2.7.0
42
- Requires-Dist: torchvision<0.22.1,>=0.22.0
43
- Requires-Dist: transformers<4.54.0,>=4.51.0
44
- Requires-Dist: xgrammar>=0.1.19
40
+ Requires-Dist: torch<2.9.0,>=2.8.0
41
+ Requires-Dist: torchaudio<2.9.0,>=2.8.0
42
+ Requires-Dist: torchvision<0.24.0,>=0.23.0
43
+ Requires-Dist: transformers>=4.55.0
44
+ Requires-Dist: xgrammar>=0.1.21
45
45
  Provides-Extra: gpu
46
46
  Requires-Dist: bitsandbytes==0.42.0; (sys_platform == 'darwin') and extra == 'gpu'
47
47
  Requires-Dist: bitsandbytes>=0.45.5; (sys_platform == 'linux') and extra == 'gpu'
48
- Requires-Dist: vllm==0.9.1; (sys_platform == 'linux' or sys_platform == 'darwin') and extra == 'gpu'
48
+ Requires-Dist: vllm==0.10.2; (sys_platform == 'linux' or sys_platform == 'darwin') and extra == 'gpu'
49
49
  Description-Content-Type: text/markdown
50
50
 
51
51
  # Synthetic Data Engine 💎
@@ -22,7 +22,7 @@ from mostlyai.engine.splitting import split
22
22
  from mostlyai.engine.training import train
23
23
 
24
24
  __all__ = ["split", "analyze", "encode", "train", "generate", "init_logging", "set_random_state"]
25
- __version__ = "1.5.6"
25
+ __version__ = "1.5.7"
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.*")
@@ -13,7 +13,6 @@
13
13
  # limitations under the License.
14
14
 
15
15
  from abc import ABC, abstractmethod
16
- from collections.abc import Generator
17
16
  from dataclasses import dataclass
18
17
 
19
18
  from pydantic import BaseModel
@@ -26,10 +25,6 @@ class EngineMetrics:
26
25
 
27
26
 
28
27
  class LanguageEngine(ABC):
29
- @abstractmethod
30
- def initialize_logits_processors(self, schemas: Generator[BaseModel]):
31
- pass
32
-
33
28
  @abstractmethod
34
29
  def generate(
35
30
  self, text: list[str], sampling_temperature: float, sampling_top_p: float
@@ -47,3 +42,24 @@ class LanguageEngine(ABC):
47
42
  @abstractmethod
48
43
  def cleanup(self):
49
44
  pass
45
+
46
+ def update_json_constraints(self, schemas: list[BaseModel] | None) -> None:
47
+ """Update JSON schema constraints for the next generation call.
48
+
49
+ Args:
50
+ schemas: Schema constraints to apply to the next generate() call.
51
+ None to clear any existing constraints.
52
+
53
+ Default implementation does nothing.
54
+ Engines that support JSON constraints should override this method.
55
+ """
56
+ pass
57
+
58
+ def can_reuse_schemas(self) -> bool:
59
+ """Whether the engine can reuse JSON schema constraints across batches with different sizes.
60
+
61
+ Returns:
62
+ True if the engine can handle variable batch sizes with reused schema constraints,
63
+ False if schema constraints need to be recreated for each batch.
64
+ """
65
+ return True
@@ -15,7 +15,6 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  import time
18
- from collections.abc import Generator
19
18
  from os import PathLike
20
19
  from pathlib import Path
21
20
 
@@ -75,15 +74,6 @@ class HuggingFaceEngine(LanguageEngine):
75
74
  def supports_json_enforcing(self) -> bool:
76
75
  return self._json_enforcing_possible
77
76
 
78
- def initialize_logits_processors(self, schemas: Generator[BaseModel]):
79
- compiled_grammars = create_compiled_grammars(
80
- schemas=schemas,
81
- tokenizer=self.tokenizer,
82
- vocab_size=self._model_config.vocab_size,
83
- is_peft_adapter=self.is_peft_adapter,
84
- )
85
- self._logits_processors = [LogitsProcessor(list(compiled_grammars))]
86
-
87
77
  def generate(
88
78
  self, text: list[str], sampling_temperature: float, sampling_top_p: float
89
79
  ) -> tuple[list[int], EngineMetrics]:
@@ -124,3 +114,20 @@ class HuggingFaceEngine(LanguageEngine):
124
114
 
125
115
  def cleanup(self):
126
116
  pass
117
+
118
+ def update_json_constraints(self, schemas: list[BaseModel] | None) -> None:
119
+ """Update JSON schema constraints for the next generation call."""
120
+ if schemas:
121
+ compiled_grammars = create_compiled_grammars(
122
+ schemas=schemas,
123
+ tokenizer=self.tokenizer,
124
+ vocab_size=self._model_config.vocab_size,
125
+ is_peft_adapter=self.is_peft_adapter,
126
+ )
127
+ self._logits_processors = [LogitsProcessor(list(compiled_grammars))]
128
+ else:
129
+ self._logits_processors = None
130
+
131
+ def can_reuse_schemas(self) -> bool:
132
+ """HuggingFaceEngine cannot reuse LogitsProcessor across different batch sizes."""
133
+ return False
@@ -0,0 +1,174 @@
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 os
18
+
19
+ os.environ["VLLM_USE_V1"] = "1"
20
+
21
+
22
+ import time
23
+ from os import PathLike
24
+
25
+ import torch
26
+ from peft import PeftConfig
27
+ from pydantic import BaseModel
28
+ from transformers import AutoConfig, AutoTokenizer
29
+ from vllm import LLM, SamplingParams
30
+ from vllm.config import _get_and_verify_max_len
31
+ from vllm.distributed import cleanup_dist_env_and_memory
32
+ from vllm.inputs.data import TokensPrompt
33
+ from vllm.lora.request import LoRARequest
34
+ from vllm.sampling_params import GuidedDecodingParams
35
+
36
+ from mostlyai.engine._language.common import is_bf16_supported
37
+ from mostlyai.engine._language.engine.base import EngineMetrics, LanguageEngine
38
+ from mostlyai.engine._language.tokenizer_utils import tokenize_fn
39
+
40
+
41
+ def get_dynamic_gpu_memory_utilization(utilization_ratio: float = 0.9) -> float:
42
+ """
43
+ Calculate dynamic GPU memory utilization based on available memory.
44
+
45
+ Args:
46
+ utilization_ratio: Fraction of available GPU memory to use (default: 0.9)
47
+
48
+ Returns:
49
+ GPU memory utilization as a fraction.
50
+ """
51
+ if not torch.cuda.is_available():
52
+ return utilization_ratio # fallback for non-GPU environments
53
+
54
+ try:
55
+ # Get free and total memory from CUDA
56
+ free_memory, total_memory = torch.cuda.mem_get_info()
57
+
58
+ # Use specified ratio of free memory
59
+ target_memory = free_memory * utilization_ratio
60
+ utilization = target_memory / total_memory
61
+
62
+ # Ensure utilization is within reasonable bounds (0.1 to 0.95)
63
+ return max(0.1, min(0.95, utilization))
64
+
65
+ except Exception:
66
+ # Fallback to provided ratio if anything goes wrong
67
+ return utilization_ratio
68
+
69
+
70
+ class VLLMEngine(LanguageEngine):
71
+ def __init__(
72
+ self, model_path: PathLike | str, device: torch.device, max_new_tokens: int, tokenizer_max_length: int
73
+ ):
74
+ self.device = device
75
+ self.tokenizer_max_length = tokenizer_max_length
76
+ self.max_new_tokens = max_new_tokens
77
+
78
+ peft_config = PeftConfig.from_pretrained(model_path)
79
+ base_config = AutoConfig.from_pretrained(peft_config.base_model_name_or_path)
80
+
81
+ model_path = str(model_path)
82
+ self._lora_request = LoRARequest("adapter", 1, model_path)
83
+ config_max_model_len = _get_and_verify_max_len(
84
+ base_config, tokenizer_config=None, max_model_len=None, disable_sliding_window=False, sliding_window=None
85
+ )
86
+
87
+ self.llm = LLM(
88
+ model=peft_config.base_model_name_or_path,
89
+ tokenizer=model_path,
90
+ max_model_len=min(config_max_model_len, self.tokenizer_max_length + max_new_tokens),
91
+ enable_lora=True,
92
+ dtype=torch.bfloat16 if is_bf16_supported(device) else torch.float16,
93
+ # enforce_eager=True, # results in big slowdown, but is needed when running pytest locally
94
+ swap_space=0,
95
+ disable_log_stats=True,
96
+ tensor_parallel_size=torch.cuda.device_count(),
97
+ gpu_memory_utilization=get_dynamic_gpu_memory_utilization(),
98
+ )
99
+ self.tokenizer = AutoTokenizer.from_pretrained(
100
+ model_path,
101
+ padding_side="left",
102
+ truncation_side="left",
103
+ legacy=True,
104
+ # these must be False at initialization, as we manually add them later in tokenize_fn
105
+ add_bos_token=False,
106
+ add_eos_token=False,
107
+ )
108
+ self._prepared_schemas = None
109
+
110
+ def get_default_batch_size(self) -> int:
111
+ return 192
112
+
113
+ def supports_json_enforcing(self) -> bool:
114
+ return True
115
+
116
+ def generate(
117
+ self, text: list[str], sampling_temperature: float, sampling_top_p: float
118
+ ) -> tuple[list[int], EngineMetrics]:
119
+ tokenize_kwargs = dict(
120
+ tokenizer=self.tokenizer,
121
+ return_tensors=None,
122
+ add_bos_token=True,
123
+ add_eos_token=False,
124
+ padding=False,
125
+ truncation=True,
126
+ max_length=self.tokenizer_max_length, # truncates input
127
+ )
128
+ t_tokenize = time.time()
129
+ inputs = tokenize_fn(text=text, **tokenize_kwargs)
130
+ tokenize_time = time.time() - t_tokenize
131
+
132
+ actual_batch_size = len(inputs["input_ids"])
133
+
134
+ # Create sampling params with guided decoding if schemas are prepared
135
+ effective_schemas = self._prepared_schemas
136
+
137
+ sampling_params = []
138
+ for i in range(actual_batch_size):
139
+ guided_decoding = None
140
+ if effective_schemas and i < len(effective_schemas):
141
+ # Convert Pydantic model to JSON schema for guided decoding
142
+ schema_dict = effective_schemas[i].model_json_schema()
143
+ guided_decoding = GuidedDecodingParams(json=schema_dict)
144
+
145
+ sampling_params.append(
146
+ SamplingParams(
147
+ max_tokens=self.max_new_tokens,
148
+ temperature=sampling_temperature,
149
+ top_p=sampling_top_p,
150
+ guided_decoding=guided_decoding,
151
+ )
152
+ )
153
+ t_generate = time.time()
154
+ outputs = self.llm.generate(
155
+ prompts=[TokensPrompt(prompt_token_ids=token_ids) for token_ids in inputs["input_ids"]],
156
+ sampling_params=sampling_params,
157
+ use_tqdm=False,
158
+ lora_request=self._lora_request,
159
+ )
160
+ generate_time = time.time() - t_generate
161
+ metrics = EngineMetrics(tokenize_time=tokenize_time, generate_time=generate_time)
162
+ return [r.outputs[0].token_ids for r in outputs], metrics
163
+
164
+ def cleanup(self):
165
+ del self.llm
166
+ cleanup_dist_env_and_memory()
167
+
168
+ def update_json_constraints(self, schemas: list[BaseModel] | None) -> None:
169
+ """Update JSON schema constraints for the next generation call."""
170
+ self._prepared_schemas = list(schemas) if schemas else None
171
+
172
+ def can_reuse_schemas(self) -> bool:
173
+ """VLLMEngine can handle variable batch sizes since it creates sampling params per sample."""
174
+ return True
@@ -287,15 +287,18 @@ def generate(
287
287
  enforce_json_output = engine.supports_json_enforcing()
288
288
  _LOG.info(f"{enforce_json_output=}")
289
289
 
290
- initialize_logits_processors_once = len(seeded_tgt_columns) == 0 and engine.__class__.__name__ == "VLLMEngine"
291
- if enforce_json_output and initialize_logits_processors_once:
290
+ # Check if we can optimize by reusing schemas/constraints across batches
291
+ can_reuse_schemas = len(seeded_tgt_columns) == 0 and engine.can_reuse_schemas()
292
+
293
+ # Prepare schemas once if optimization is possible
294
+ if enforce_json_output and can_reuse_schemas:
292
295
  t0 = time.time()
293
- schemas = create_schemas(
296
+ schemas_for_optimization = create_schemas(
294
297
  size=batch_size,
295
298
  stats=tgt_stats,
296
299
  rare_category_replacement_method=rare_category_replacement_method,
297
300
  )
298
- engine.initialize_logits_processors(schemas=schemas)
301
+ engine.update_json_constraints(schemas_for_optimization)
299
302
  total_logits_processor_build_time += time.time() - t0
300
303
 
301
304
  # keep at most 500k samples in memory before decoding and writing to disk
@@ -309,16 +312,21 @@ def generate(
309
312
  ctx_batch = ctx_data.iloc[samples_processed : samples_processed + batch_size]
310
313
  ctx_keys = ctx_batch[ctx_primary_key]
311
314
 
312
- if enforce_json_output and not initialize_logits_processors_once:
315
+ # Update JSON constraints if needed per-batch (when schema reuse is not possible)
316
+ if enforce_json_output and not can_reuse_schemas:
313
317
  t0 = time.time()
314
318
  schemas = create_schemas(
315
319
  seed_df=seed_data_batch,
316
320
  stats=tgt_stats,
317
321
  rare_category_replacement_method=rare_category_replacement_method,
318
322
  )
319
- engine.initialize_logits_processors(schemas=schemas)
323
+ engine.update_json_constraints(schemas)
320
324
  total_logits_processor_build_time += time.time() - t0
325
+ elif not enforce_json_output:
326
+ # Clear any existing constraints if JSON enforcement is disabled
327
+ engine.update_json_constraints(None)
321
328
 
329
+ # Generate outputs using single generate method
322
330
  outputs, metrics = engine.generate(
323
331
  encoded_ctx_batch["ctx"].tolist(),
324
332
  sampling_temperature=sampling_temperature,
@@ -25,6 +25,11 @@ _LOG = logging.getLogger(__name__)
25
25
  class LSTMFromScratchConfig(PretrainedConfig):
26
26
  model_type = model_id = "MOSTLY_AI/LSTMFromScratch-3m"
27
27
 
28
+ # Map standard transformer attributes to our custom LSTM attributes
29
+ attribute_map = {
30
+ "num_hidden_layers": "num_layers",
31
+ }
32
+
28
33
  def __init__(
29
34
  self,
30
35
  vocab_size: int | None = None,
@@ -63,6 +63,7 @@ from mostlyai.engine._training_utils import (
63
63
  ModelCheckpoint,
64
64
  ProgressMessage,
65
65
  check_early_training_exit,
66
+ gpu_memory_cleanup,
66
67
  )
67
68
  from mostlyai.engine._workspace import Workspace, ensure_workspace_dir
68
69
  from mostlyai.engine.domain import DifferentialPrivacyConfig, ModelStateStrategy
@@ -277,6 +278,7 @@ def _gpu_estimate_max_batch_size(
277
278
  return batch_size
278
279
 
279
280
 
281
+ @gpu_memory_cleanup
280
282
  def train(
281
283
  *,
282
284
  model: str = "MOSTLY_AI/LSTMFromScratch-3m",
@@ -865,4 +867,5 @@ def train(
865
867
  )
866
868
  # ensure everything gets uploaded
867
869
  upload_model_data_callback()
870
+
868
871
  _LOG.info(f"TRAIN_LANGUAGE finished in {time.time() - t0_:.2f}s")
@@ -63,6 +63,7 @@ from mostlyai.engine._training_utils import (
63
63
  ModelCheckpoint,
64
64
  ProgressMessage,
65
65
  check_early_training_exit,
66
+ gpu_memory_cleanup,
66
67
  )
67
68
  from mostlyai.engine._workspace import Workspace, ensure_workspace_dir
68
69
  from mostlyai.engine.domain import DifferentialPrivacyConfig, ModelStateStrategy
@@ -337,6 +338,7 @@ def _calculate_average_trn_loss(trn_sample_losses: list[torch.Tensor], n: int |
337
338
  ################
338
339
 
339
340
 
341
+ @gpu_memory_cleanup
340
342
  def train(
341
343
  *,
342
344
  model: str = "MOSTLY_AI/Medium",
@@ -867,4 +869,5 @@ def train(
867
869
  progress.update(completed=steps, total=steps, message=progress_message)
868
870
  # ensure everything gets uploaded
869
871
  upload_model_data_callback()
872
+
870
873
  _LOG.info(f"TRAIN_TABULAR finished in {time.time() - t0:.2f}s")
@@ -13,6 +13,8 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import abc
16
+ import functools
17
+ import gc
16
18
  import logging
17
19
  import time
18
20
 
@@ -157,3 +159,18 @@ def check_early_training_exit(workspace: Workspace, trn_cnt: int, val_cnt: int)
157
159
  trn_files = workspace.encoded_data_trn.fetch_all()
158
160
  val_files = workspace.encoded_data_val.fetch_all()
159
161
  return any((len(trn_files) == 0, len(val_files) == 0, trn_cnt == 0, val_cnt == 0))
162
+
163
+
164
+ def gpu_memory_cleanup(func):
165
+ """Decorator to clean up GPU memory after function execution."""
166
+
167
+ @functools.wraps(func)
168
+ def wrapper(*args, **kwargs):
169
+ try:
170
+ return func(*args, **kwargs)
171
+ finally:
172
+ for _ in range(5):
173
+ gc.collect()
174
+ torch.cuda.empty_cache()
175
+
176
+ return wrapper
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mostlyai-engine"
3
- version = "1.5.6"
3
+ version = "1.5.7"
4
4
  description = "Synthetic Data Engine"
5
5
  authors = [{ name = "MOSTLY AI", email = "dev@mostly.ai" }]
6
6
  requires-python = ">=3.10"
@@ -25,31 +25,31 @@ classifiers = [
25
25
  ]
26
26
 
27
27
  dependencies = [
28
- "setuptools>=77.0.3", # similar to vllm 0.9.1
28
+ "setuptools>=77.0.3", # similar to vllm 0.10.1.1
29
29
  "numpy>=2.0.0",
30
30
  "pandas~=2.2.0",
31
31
  "pyarrow>=16.0.0",
32
32
  "joblib>=1.4.2",
33
33
  "psutil>=5.9.5,<6", # upgrade when colab psutil is updated
34
34
  "tokenizers>=0.21.0",
35
- "transformers>=4.51.0,<4.54.0", # update when vllm is updated to >0.10.1
35
+ "transformers>=4.55.0", # updated (removed ,<4.54.0), similar to vllm 0.10.1.1
36
36
  "datasets>=3.0.0",
37
37
  "accelerate>=1.5.0",
38
38
  "peft>=0.12.0",
39
39
  "huggingface-hub[hf-xet]>=0.30.2",
40
40
  "opacus>=1.5.4",
41
- "xgrammar>=0.1.19", # for vllm 0.9.1 compatibility
41
+ "xgrammar>=0.1.21", # for vllm 0.10.1.1 compatibility (=0.1.21)
42
42
  "json-repair>=0.47.0",
43
- "torch>=2.7.0,<2.7.1", # for vllm 0.9.1 compatibility
44
- "torchaudio>=2.7.0,<2.7.1", # for vllm 0.9.1 compatibility
45
- "torchvision>=0.22.0,<0.22.1" # for vllm 0.9.1 compatibility
43
+ "torch>=2.8.0,<2.9.0", # for vllm 0.10.2 compatibility
44
+ "torchaudio>=2.8.0,<2.9.0", # for vllm 0.10.2 compatibility
45
+ "torchvision>=0.23.0,<0.24.0" # for vllm 0.10.2 compatibility
46
46
  ]
47
47
 
48
48
  [project.optional-dependencies]
49
49
  gpu = [
50
50
  "bitsandbytes==0.42.0; sys_platform == 'darwin'",
51
51
  "bitsandbytes>=0.45.5; sys_platform == 'linux'",
52
- "vllm==0.9.1; sys_platform == 'linux' or sys_platform == 'darwin'",
52
+ "vllm==0.10.2; sys_platform == 'linux' or sys_platform == 'darwin'",
53
53
  ]
54
54
 
55
55
  [dependency-groups]
@@ -1,223 +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
- from __future__ import annotations
16
-
17
- import os
18
-
19
- # Per-Request Logits Processors are not supported in V1 (https://docs.vllm.ai/en/latest/getting_started/v1_user_guide.html#feature-model)
20
- # Global Logits Processors will be new mechanism to use, but it is not yet released
21
- # Here is PR for it: https://github.com/vllm-project/vllm/pull/13360
22
- os.environ["VLLM_USE_V1"] = "0"
23
-
24
- import contextlib
25
- import gc
26
- import time
27
- from collections.abc import Generator
28
- from os import PathLike
29
-
30
- import torch
31
- import xgrammar as xgr
32
- from peft import PeftConfig
33
- from pydantic import BaseModel
34
- from transformers import AutoConfig, AutoTokenizer
35
- from vllm import LLM, SamplingParams
36
- from vllm.config import _get_and_verify_max_len
37
- from vllm.distributed import destroy_distributed_environment, destroy_model_parallel
38
- from vllm.lora.request import LoRARequest
39
- from vllm.platforms import current_platform
40
-
41
- from mostlyai.engine._language.common import is_bf16_supported
42
- from mostlyai.engine._language.engine.base import EngineMetrics, LanguageEngine
43
- from mostlyai.engine._language.tokenizer_utils import tokenize_fn
44
- from mostlyai.engine._language.xgrammar_utils import create_compiled_grammars
45
-
46
-
47
- def cleanup_dist_env_and_memory():
48
- """Copy from current main of vllm replace by import when possible"""
49
- destroy_model_parallel()
50
- destroy_distributed_environment()
51
- with contextlib.suppress(AssertionError):
52
- torch.distributed.destroy_process_group()
53
- gc.collect()
54
- if not current_platform.is_cpu():
55
- torch.cuda.empty_cache()
56
-
57
-
58
- class XGrammarLogitsProcessor:
59
- """
60
- Inspired by [XGrammarLogitsProcessor](https://github.com/vllm-project/vllm/blob/a43aa183dc0cb2639044c15d272e0ce1941392b0/vllm/model_executor/guided_decoding/xgrammar_decoding.py#L280).
61
- VLLM's XGrammarLogitsProcessor can be reused.
62
- """
63
-
64
- def __init__(self, compiled_grammar: xgr.CompiledGrammar):
65
- self.compiled_grammar = compiled_grammar
66
- self.tokenizer_info = compiled_grammar.tokenizer_info
67
- self.batch_size = 1
68
-
69
- self.matchers: list[xgr.GrammarMatcher] | None = None
70
- self.token_bitmask: torch.Tensor | None = None
71
- self.prefilled = False
72
-
73
- def __call__(self, input_ids: tuple[int], scores: torch.Tensor) -> torch.Tensor:
74
- # lazily initialize GrammarMatchers and bitmask
75
- if self.matchers is None:
76
- self.matchers = [xgr.GrammarMatcher(self.compiled_grammar) for _ in range(self.batch_size)]
77
- self.token_bitmask = xgr.allocate_token_bitmask(self.batch_size, self.tokenizer_info.vocab_size)
78
-
79
- if not self.prefilled:
80
- # have not sampled a token yet
81
- self.prefilled = True
82
- else:
83
- for i, matcher in enumerate(self.matchers):
84
- if not matcher.is_terminated():
85
- sampled_token = input_ids[-1]
86
- assert self.matchers[i].accept_token(sampled_token)
87
-
88
- for i, matcher in enumerate(self.matchers):
89
- if not matcher.is_terminated():
90
- matcher.fill_next_token_bitmask(self.token_bitmask, i)
91
-
92
- # token_bitmask is a CPU tensor for use with accept_token and
93
- # fill_next_token_bitmask so we move it to the device of scores
94
- device_type = scores.device.type
95
- dtype = scores.dtype
96
- if device_type != "cuda":
97
- # xgrammar on cpu only supports float32 scores
98
- # see: https://github.com/mlc-ai/xgrammar/blob/c1b64920cad24f44f235778c1c00bb52d57da01a/python/xgrammar/kernels/apply_token_bitmask_inplace_cpu.py#L22
99
- scores = scores.to("cpu").float().unsqueeze(0)
100
-
101
- # Note: In this method, if the tensors have different dimensions
102
- # on CPU device fails, but on GPU it runs without error. Hence the
103
- # unsqueeze above for scores, to match the token bitmask shape
104
- xgr.apply_token_bitmask_inplace(scores, self.token_bitmask.to(scores.device, non_blocking=True))
105
- if device_type != "cuda":
106
- scores = scores.to(dtype).to(device_type).squeeze()
107
- return scores
108
-
109
- def clone(self) -> XGrammarLogitsProcessor:
110
- """Create a new instance with shared compiled grammar but separate state"""
111
- new_processor = XGrammarLogitsProcessor(self.compiled_grammar)
112
-
113
- # create fresh matchers for the new sequence
114
- if self.matchers is not None:
115
- new_processor.matchers = [xgr.GrammarMatcher(self.compiled_grammar) for _ in range(self.batch_size)]
116
-
117
- # create a new token bitmask with the same size
118
- if self.token_bitmask is not None:
119
- new_processor.token_bitmask = self.token_bitmask
120
-
121
- new_processor.batch_size = self.batch_size
122
-
123
- # reset prefilled state for new sequence
124
- new_processor.prefilled = False
125
-
126
- return new_processor
127
-
128
-
129
- class VLLMEngine(LanguageEngine):
130
- def __init__(
131
- self, model_path: PathLike | str, device: torch.device, max_new_tokens: int, tokenizer_max_length: int
132
- ):
133
- self.device = device
134
- self.tokenizer_max_length = tokenizer_max_length
135
- self.max_new_tokens = max_new_tokens
136
-
137
- peft_config = PeftConfig.from_pretrained(model_path)
138
- base_config = AutoConfig.from_pretrained(peft_config.base_model_name_or_path)
139
-
140
- model_path = str(model_path)
141
- self._lora_request = LoRARequest("adapter", 1, model_path)
142
- config_max_model_len = _get_and_verify_max_len(
143
- base_config, max_model_len=None, disable_sliding_window=None, sliding_window_len=None
144
- )
145
- self.llm = LLM(
146
- model=peft_config.base_model_name_or_path,
147
- tokenizer=model_path,
148
- device=device.type,
149
- max_model_len=min(config_max_model_len, self.tokenizer_max_length + max_new_tokens),
150
- enable_lora=True,
151
- dtype=torch.bfloat16 if is_bf16_supported(device) else torch.float16,
152
- # enforce_eager=True, # results in big slowdown, but is needed when running pytest locally
153
- swap_space=0,
154
- disable_log_stats=True,
155
- tensor_parallel_size=torch.cuda.device_count(),
156
- )
157
- self.tokenizer = AutoTokenizer.from_pretrained(
158
- model_path,
159
- padding_side="left",
160
- truncation_side="left",
161
- legacy=True,
162
- # these must be False at initialization, as we manually add them later in tokenize_fn
163
- add_bos_token=False,
164
- add_eos_token=False,
165
- )
166
- self._logits_processors = None
167
-
168
- def get_default_batch_size(self) -> int:
169
- return 192
170
-
171
- def supports_json_enforcing(self) -> bool:
172
- return True
173
-
174
- def initialize_logits_processors(self, schemas: Generator[BaseModel]):
175
- compiled_grammars = create_compiled_grammars(
176
- schemas=schemas,
177
- tokenizer=self.llm.get_tokenizer(),
178
- vocab_size=self.llm.llm_engine.get_model_config().get_vocab_size(),
179
- is_peft_adapter=True,
180
- )
181
- self._logits_processors = [XGrammarLogitsProcessor(compiled_grammar) for compiled_grammar in compiled_grammars]
182
-
183
- def generate(
184
- self, text: list[str], sampling_temperature: float, sampling_top_p: float
185
- ) -> tuple[list[int], EngineMetrics]:
186
- tokenize_kwargs = dict(
187
- tokenizer=self.tokenizer,
188
- return_tensors=None,
189
- add_bos_token=True,
190
- add_eos_token=False,
191
- padding=False,
192
- truncation=True,
193
- max_length=self.tokenizer_max_length, # truncates input
194
- )
195
- t_tokenize = time.time()
196
- inputs = tokenize_fn(text=text, **tokenize_kwargs)
197
- tokenize_time = time.time() - t_tokenize
198
-
199
- actual_batch_size = len(inputs["input_ids"])
200
- sampling_params = [
201
- SamplingParams(
202
- max_tokens=self.max_new_tokens,
203
- temperature=sampling_temperature,
204
- top_p=sampling_top_p,
205
- logits_processors=[lp],
206
- )
207
- for lp in self._logits_processors[:actual_batch_size]
208
- ]
209
- t_generate = time.time()
210
- outputs = self.llm.generate(
211
- prompts=None,
212
- prompt_token_ids=inputs["input_ids"],
213
- sampling_params=sampling_params,
214
- use_tqdm=False,
215
- lora_request=self._lora_request,
216
- )
217
- generate_time = time.time() - t_generate
218
- metrics = EngineMetrics(tokenize_time=tokenize_time, generate_time=generate_time)
219
- return [r.outputs[0].token_ids for r in outputs], metrics
220
-
221
- def cleanup(self):
222
- del self.llm
223
- cleanup_dist_env_and_memory()
File without changes