synapse-sdk 2025.9.5__py3-none-any.whl → 2025.10.6__py3-none-any.whl

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.

Potentially problematic release.


This version of synapse-sdk might be problematic. Click here for more details.

Files changed (78) hide show
  1. synapse_sdk/clients/base.py +129 -9
  2. synapse_sdk/devtools/docs/docs/api/clients/base.md +230 -8
  3. synapse_sdk/devtools/docs/docs/api/plugins/models.md +58 -3
  4. synapse_sdk/devtools/docs/docs/plugins/categories/neural-net-plugins/train-action-overview.md +663 -0
  5. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/pre-annotation-plugin-overview.md +198 -0
  6. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-action-development.md +1645 -0
  7. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-overview.md +717 -0
  8. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-template-development.md +1380 -0
  9. synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-action.md +934 -0
  10. synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-overview.md +585 -0
  11. synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-template.md +715 -0
  12. synapse_sdk/devtools/docs/docs/plugins/export-plugins.md +39 -0
  13. synapse_sdk/devtools/docs/docs/plugins/plugins.md +12 -5
  14. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/api/clients/base.md +230 -8
  15. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/api/plugins/models.md +114 -0
  16. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/neural-net-plugins/train-action-overview.md +621 -0
  17. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/pre-annotation-plugin-overview.md +198 -0
  18. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-action-development.md +1645 -0
  19. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-overview.md +717 -0
  20. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-template-development.md +1380 -0
  21. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-action.md +934 -0
  22. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-overview.md +585 -0
  23. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-template.md +715 -0
  24. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/export-plugins.md +39 -0
  25. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current.json +16 -4
  26. synapse_sdk/devtools/docs/sidebars.ts +45 -1
  27. synapse_sdk/plugins/README.md +487 -80
  28. synapse_sdk/plugins/categories/base.py +1 -0
  29. synapse_sdk/plugins/categories/export/actions/export/action.py +8 -3
  30. synapse_sdk/plugins/categories/export/actions/export/utils.py +108 -8
  31. synapse_sdk/plugins/categories/export/templates/config.yaml +18 -0
  32. synapse_sdk/plugins/categories/export/templates/plugin/export.py +97 -0
  33. synapse_sdk/plugins/categories/neural_net/actions/train.py +592 -22
  34. synapse_sdk/plugins/categories/neural_net/actions/tune.py +150 -3
  35. synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +4 -0
  36. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/__init__.py +3 -0
  37. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/action.py +10 -0
  38. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/__init__.py +28 -0
  39. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/action.py +145 -0
  40. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/enums.py +269 -0
  41. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/exceptions.py +14 -0
  42. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/factory.py +76 -0
  43. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py +97 -0
  44. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/orchestrator.py +250 -0
  45. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/run.py +64 -0
  46. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/__init__.py +17 -0
  47. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/annotation.py +284 -0
  48. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/base.py +170 -0
  49. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/extraction.py +83 -0
  50. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/metrics.py +87 -0
  51. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/preprocessor.py +127 -0
  52. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/validation.py +143 -0
  53. synapse_sdk/plugins/categories/upload/actions/upload/__init__.py +2 -1
  54. synapse_sdk/plugins/categories/upload/actions/upload/action.py +8 -1
  55. synapse_sdk/plugins/categories/upload/actions/upload/context.py +0 -1
  56. synapse_sdk/plugins/categories/upload/actions/upload/models.py +134 -94
  57. synapse_sdk/plugins/categories/upload/actions/upload/steps/cleanup.py +2 -2
  58. synapse_sdk/plugins/categories/upload/actions/upload/steps/generate.py +6 -2
  59. synapse_sdk/plugins/categories/upload/actions/upload/steps/initialize.py +24 -9
  60. synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py +130 -18
  61. synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py +147 -37
  62. synapse_sdk/plugins/categories/upload/actions/upload/steps/upload.py +10 -5
  63. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/flat.py +31 -6
  64. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/recursive.py +65 -37
  65. synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/default.py +17 -2
  66. synapse_sdk/plugins/categories/upload/templates/README.md +394 -0
  67. synapse_sdk/plugins/models.py +62 -0
  68. synapse_sdk/utils/file/download.py +261 -0
  69. {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/METADATA +15 -2
  70. {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/RECORD +74 -43
  71. synapse_sdk/devtools/docs/docs/plugins/developing-upload-template.md +0 -1463
  72. synapse_sdk/devtools/docs/docs/plugins/upload-plugins.md +0 -1964
  73. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/developing-upload-template.md +0 -1463
  74. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/upload-plugins.md +0 -2077
  75. {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/WHEEL +0 -0
  76. {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/entry_points.txt +0 -0
  77. {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/licenses/LICENSE +0 -0
  78. {synapse_sdk-2025.9.5.dist-info → synapse_sdk-2025.10.6.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,9 @@
1
1
  import copy
2
2
  import tempfile
3
- from decimal import Decimal
4
3
  from pathlib import Path
5
- from typing import Annotated
4
+ from typing import Annotated, Optional
6
5
 
7
- from pydantic import AfterValidator, BaseModel, field_validator
6
+ from pydantic import AfterValidator, BaseModel, field_validator, model_validator
8
7
  from pydantic_core import PydanticCustomError
9
8
 
10
9
  from synapse_sdk.clients.exceptions import ClientError
@@ -13,57 +12,359 @@ from synapse_sdk.plugins.categories.decorators import register_action
13
12
  from synapse_sdk.plugins.enums import PluginCategory, RunMethod
14
13
  from synapse_sdk.plugins.models import Run
15
14
  from synapse_sdk.utils.file import archive, get_temp_path, unarchive
15
+ from synapse_sdk.utils.module_loading import import_string
16
16
  from synapse_sdk.utils.pydantic.validators import non_blank
17
17
 
18
18
 
19
19
  class TrainRun(Run):
20
+ is_tune = False
21
+ completed_samples = 0
22
+ num_samples = 0
23
+ checkpoint_output = None
24
+
20
25
  def log_metric(self, category, key, value, **metrics):
21
26
  # TODO validate input via plugin config
22
- self.log('metric', {'category': category, 'key': key, 'value': value, 'metrics': metrics})
27
+ data = {'category': category, 'key': key, 'value': value, 'metrics': metrics}
28
+
29
+ # Automatically add trial_id when is_tune=True
30
+ if self.is_tune:
31
+ try:
32
+ from ray import train
33
+
34
+ context = train.get_context()
35
+ trial_id = context.get_trial_id()
36
+ if trial_id:
37
+ data['trial_id'] = trial_id
38
+ except Exception:
39
+ # If Ray context is not available, continue without trial_id
40
+ pass
41
+
42
+ self.log('metric', data)
23
43
 
24
44
  def log_visualization(self, category, group, index, image, **meta):
25
45
  # TODO validate input via plugin config
26
- self.log('visualization', {'category': category, 'group': group, 'index': index, **meta}, file=image)
46
+ data = {'category': category, 'group': group, 'index': index, **meta}
47
+
48
+ # Automatically add trial_id when is_tune=True
49
+ if self.is_tune:
50
+ try:
51
+ from ray import train
52
+
53
+ context = train.get_context()
54
+ trial_id = context.get_trial_id()
55
+ if trial_id:
56
+ data['trial_id'] = trial_id
57
+ except Exception:
58
+ # If Ray context is not available, continue without trial_id
59
+ pass
60
+
61
+ self.log('visualization', data, file=image)
62
+
63
+
64
+ class SearchAlgo(BaseModel):
65
+ """
66
+ Configuration for Ray Tune search algorithms.
67
+
68
+ Supported algorithms:
69
+ - 'bayesoptsearch': Bayesian optimization using Gaussian Processes
70
+ - 'hyperoptsearch': Tree-structured Parzen Estimator (TPE)
71
+ - 'basicvariantgenerator': Random search (default)
72
+
73
+ Attributes:
74
+ name (str): Name of the search algorithm (case-insensitive)
75
+ points_to_evaluate (Optional[dict]): Optional initial hyperparameter
76
+ configurations to evaluate before starting optimization
77
+
78
+ Example:
79
+ {
80
+ "name": "hyperoptsearch",
81
+ "points_to_evaluate": [
82
+ {"learning_rate": 0.001, "batch_size": 32}
83
+ ]
84
+ }
85
+ """
86
+
87
+ name: str
88
+ points_to_evaluate: Optional[dict] = None
89
+
90
+
91
+ class Scheduler(BaseModel):
92
+ """
93
+ Configuration for Ray Tune schedulers.
94
+
95
+ Supported schedulers:
96
+ - 'fifo': First-In-First-Out scheduler (default, runs all trials)
97
+ - 'hyperband': HyperBand early stopping scheduler
27
98
 
99
+ Attributes:
100
+ name (str): Name of the scheduler (case-insensitive)
101
+ options (Optional[str]): Optional scheduler-specific configuration parameters
28
102
 
29
- class Hyperparameter(BaseModel):
30
- batch_size: int
31
- epochs: int
32
- learning_rate: Decimal
103
+ Example:
104
+ {
105
+ "name": "hyperband",
106
+ "options": {
107
+ "max_t": 100,
108
+ "reduction_factor": 3
109
+ }
110
+ }
111
+ """
112
+
113
+ name: str
114
+ options: Optional[str] = None
115
+
116
+
117
+ class TuneConfig(BaseModel):
118
+ """
119
+ Configuration for Ray Tune hyperparameter optimization.
120
+
121
+ Used when is_tune=True to configure the hyperparameter search process.
122
+
123
+ Attributes:
124
+ mode (Optional[str]): Optimization mode - 'max' or 'min'
125
+ metric (Optional[str]): Name of the metric to optimize
126
+ num_samples (int): Number of hyperparameter configurations to try (default: 1)
127
+ max_concurrent_trials (Optional[int]): Maximum number of trials to run in parallel
128
+ search_alg (Optional[SearchAlgo]): Search algorithm configuration
129
+ scheduler (Optional[Scheduler]): Trial scheduler configuration
130
+
131
+ Example:
132
+ {
133
+ "mode": "max",
134
+ "metric": "accuracy",
135
+ "num_samples": 20,
136
+ "max_concurrent_trials": 4,
137
+ "search_alg": {
138
+ "name": "hyperoptsearch"
139
+ },
140
+ "scheduler": {
141
+ "name": "hyperband",
142
+ "options": {"max_t": 100}
143
+ }
144
+ }
145
+ """
146
+
147
+ mode: Optional[str] = None
148
+ metric: Optional[str] = None
149
+ num_samples: int = 1
150
+ max_concurrent_trials: Optional[int] = None
151
+ search_alg: Optional[SearchAlgo] = None
152
+ scheduler: Optional[Scheduler] = None
33
153
 
34
154
 
35
155
  class TrainParams(BaseModel):
156
+ """
157
+ Parameters for TrainAction supporting both regular training and hyperparameter tuning.
158
+
159
+ Attributes:
160
+ name (str): Name for the training/tuning job
161
+ description (str): Description of the job
162
+ checkpoint (int | None): Optional checkpoint ID to resume from
163
+ dataset (int): Dataset ID to use for training
164
+ is_tune (bool): Enable hyperparameter tuning mode (default: False)
165
+ tune_config (Optional[TuneConfig]): Tune configuration (required when is_tune=True)
166
+ num_cpus (Optional[int]): CPUs per trial (tuning mode only)
167
+ num_gpus (Optional[int]): GPUs per trial (tuning mode only)
168
+ hyperparameter (Optional[Any]): Fixed hyperparameters (required when is_tune=False)
169
+ hyperparameters (Optional[list]): Hyperparameter search space (required when is_tune=True)
170
+
171
+ Hyperparameter format when is_tune=True:
172
+ Each item in hyperparameters list must have:
173
+ - 'name': Parameter name (string)
174
+ - 'type': Distribution type (string)
175
+ - Type-specific parameters:
176
+ - uniform/quniform: 'min', 'max'
177
+ - loguniform/qloguniform: 'min', 'max', 'base'
178
+ - randn/qrandn: 'mean', 'sd'
179
+ - randint/qrandint: 'min', 'max'
180
+ - lograndint/qlograndint: 'min', 'max', 'base'
181
+ - choice/grid_search: 'options'
182
+
183
+ Example (Training mode):
184
+ {
185
+ "name": "my_training",
186
+ "dataset": 123,
187
+ "is_tune": false,
188
+ "hyperparameter": {
189
+ "epochs": 100,
190
+ "batch_size": 32,
191
+ "learning_rate": 0.001
192
+ }
193
+ }
194
+
195
+ Example (Tuning mode):
196
+ {
197
+ "name": "my_tuning",
198
+ "dataset": 123,
199
+ "is_tune": true,
200
+ "hyperparameters": [
201
+ {"name": "batch_size", "type": "choice", "options": [16, 32, 64]},
202
+ {"name": "learning_rate", "type": "loguniform", "min": 0.0001, "max": 0.01, "base": 10},
203
+ {"name": "epochs", "type": "randint", "min": 5, "max": 15}
204
+ ],
205
+ "tune_config": {
206
+ "mode": "max",
207
+ "metric": "accuracy",
208
+ "num_samples": 10
209
+ }
210
+ }
211
+ """
212
+
36
213
  name: Annotated[str, AfterValidator(non_blank)]
37
214
  description: str
38
215
  checkpoint: int | None
39
216
  dataset: int
40
- hyperparameter: Hyperparameter
217
+ is_tune: bool = False
218
+ tune_config: Optional[TuneConfig] = None
219
+ num_cpus: Optional[int] = None
220
+ num_gpus: Optional[int] = None
221
+ hyperparameter: Optional[dict] = None # plan to be deprecated
222
+ hyperparameters: Optional[list] = None
223
+
224
+ @field_validator('hyperparameter', mode='before')
225
+ @classmethod
226
+ def validate_hyperparameter(cls, v, info):
227
+ """Validate hyperparameter for train mode (is_tune=False)"""
228
+ # Get is_tune flag to determine if this field should be validated
229
+ is_tune = info.data.get('is_tune', False)
230
+
231
+ # If is_tune=True, hyperparameter should be None/not used
232
+ # Just return whatever was passed (will be validated in model_validator)
233
+ if is_tune:
234
+ return v
235
+
236
+ # For train mode, hyperparameter should be a dict
237
+ if isinstance(v, dict):
238
+ return v
239
+ elif isinstance(v, list):
240
+ raise ValueError(
241
+ 'hyperparameter must be a dict, not a list. '
242
+ 'If you want to use hyperparameter tuning, '
243
+ 'set "is_tune": true and use "hyperparameters" instead.'
244
+ )
245
+ else:
246
+ raise ValueError('hyperparameter must be a dict')
247
+
248
+ @field_validator('hyperparameters', mode='before')
249
+ @classmethod
250
+ def validate_hyperparameters(cls, v, info):
251
+ """Validate hyperparameters for tune mode (is_tune=True)"""
252
+ # Get is_tune flag to determine if this field should be validated
253
+ is_tune = info.data.get('is_tune', False)
254
+
255
+ # If is_tune=False, hyperparameters should be None/not used
256
+ # Just return whatever was passed (will be validated in model_validator)
257
+ if not is_tune:
258
+ return v
259
+
260
+ # For tune mode, hyperparameters should be a list
261
+ if isinstance(v, list):
262
+ return v
263
+ elif isinstance(v, dict):
264
+ raise ValueError(
265
+ 'hyperparameters must be a list, not a dict. '
266
+ 'If you want to use fixed hyperparameters for training, '
267
+ 'set "is_tune": false and use "hyperparameter" instead.'
268
+ )
269
+ else:
270
+ raise ValueError('hyperparameters must be a list')
41
271
 
42
272
  @field_validator('name')
43
273
  @staticmethod
44
274
  def unique_name(value, info):
45
275
  action = info.context['action']
46
276
  client = action.client
277
+ is_tune = info.data.get('is_tune', False)
47
278
  try:
48
- model_exists = client.exists('list_models', params={'name': value})
49
- job_exists = client.exists(
50
- 'list_jobs',
51
- params={
52
- 'ids_ex': action.job_id,
53
- 'category': 'neural_net',
54
- 'job__action': 'train',
55
- 'is_active': True,
56
- 'params': f'name:{value.replace(":", "%3A")}',
57
- },
58
- )
59
- assert not model_exists and not job_exists, '존재하는 학습 이름입니다.'
279
+ if not is_tune:
280
+ model_exists = client.exists('list_models', params={'name': value})
281
+ job_exists = client.exists(
282
+ 'list_jobs',
283
+ params={
284
+ 'ids_ex': action.job_id,
285
+ 'category': 'neural_net',
286
+ 'job__action': 'train',
287
+ 'is_active': True,
288
+ 'params': f'name:{value.replace(":", "%3A")}',
289
+ },
290
+ )
291
+ assert not model_exists and not job_exists, '존재하는 학습 이름입니다.'
292
+ else:
293
+ job_exists = client.exists(
294
+ 'list_jobs',
295
+ params={
296
+ 'ids_ex': action.job_id,
297
+ 'category': 'neural_net',
298
+ 'job__action': 'train',
299
+ 'is_active': True,
300
+ 'params': f'name:{value}',
301
+ },
302
+ )
303
+ assert not job_exists, '존재하는 튜닝 작업 이름입니다.'
60
304
  except ClientError:
61
305
  raise PydanticCustomError('client_error', '')
62
306
  return value
63
307
 
308
+ @model_validator(mode='after')
309
+ def validate_tune_params(self):
310
+ if self.is_tune:
311
+ # When is_tune=True, hyperparameters is required
312
+ if self.hyperparameters is None:
313
+ raise ValueError('hyperparameters is required when is_tune=True')
314
+ if self.hyperparameter is not None:
315
+ raise ValueError('hyperparameter should not be provided when is_tune=True, use hyperparameters instead')
316
+ if self.tune_config is None:
317
+ raise ValueError('tune_config is required when is_tune=True')
318
+ else:
319
+ # When is_tune=False, hyperparameter is required
320
+ if self.hyperparameter is None:
321
+ raise ValueError('hyperparameter is required when is_tune=False')
322
+ if self.hyperparameters is not None:
323
+ raise ValueError(
324
+ 'hyperparameters should not be provided when is_tune=False, use hyperparameter instead'
325
+ )
326
+ return self
327
+
64
328
 
65
329
  @register_action
66
330
  class TrainAction(Action):
331
+ """
332
+ **Important notes when using train with is_tune=True:**
333
+
334
+ 1. Path to the model output (which is the return value of your train function)
335
+ should be set to the checkpoint_output attribute of the run object **before**
336
+ starting the training.
337
+ 2. Before exiting the training function, report the results to Tune.
338
+ 3. When using own tune.py, take note of the difference in the order of parameters.
339
+ tune() function starts with hyperparameter, run, dataset, checkpoint, **kwargs
340
+ whereas the train() function starts with run, dataset, hyperparameter, checkpoint, **kwargs.
341
+ ----
342
+ 1)
343
+ Set the output path for the checkpoint to export best model
344
+
345
+ output_path = Path('path/to/your/weights')
346
+ run.checkpoint_output = str(output_path)
347
+
348
+ 2)
349
+ Before exiting the training function, report the results to Tune.
350
+ The results_dict should contain the metrics you want to report.
351
+
352
+ Example: (In train function)
353
+ results_dict = {
354
+ "accuracy": accuracy,
355
+ "loss": loss,
356
+ # Add other metrics as needed
357
+ }
358
+ if hasattr(self.dm_run, 'is_tune') and self.dm_run.is_tune:
359
+ tune.report(results_dict, checkpoint=tune.Checkpoint.from_directory(self.dm_run.checkpoint_output))
360
+
361
+
362
+ 3)
363
+ tune() function takes hyperparameter, run, dataset, checkpoint, **kwargs in that order
364
+ whereas train() function takes run, dataset, hyperparameter, checkpoint, **kwargs in that order.
365
+
366
+ """
367
+
67
368
  name = 'train'
68
369
  category = PluginCategory.NEURAL_NET
69
370
  method = RunMethod.JOB
@@ -76,12 +377,22 @@ class TrainAction(Action):
76
377
  'train': {
77
378
  'proportion': 75,
78
379
  },
380
+ 'trials': {
381
+ 'proportion': 90,
382
+ },
79
383
  'model_upload': {
80
384
  'proportion': 5,
81
385
  },
82
386
  }
83
387
 
84
388
  def start(self):
389
+ if self.params.get('is_tune', False):
390
+ return self._start_tune()
391
+ else:
392
+ return self._start_train()
393
+
394
+ def _start_train(self):
395
+ """Original train logic"""
85
396
  hyperparameter = self.params['hyperparameter']
86
397
 
87
398
  # download dataset
@@ -107,6 +418,87 @@ class TrainAction(Action):
107
418
  self.run.end_log()
108
419
  return {'model_id': model['id'] if model else None}
109
420
 
421
+ def _start_tune(self):
422
+ """Tune logic using Ray Tune for hyperparameter optimization"""
423
+ from ray import tune
424
+
425
+ # Mark run as tune
426
+ self.run.is_tune = True
427
+
428
+ # download dataset
429
+ self.run.log_message('Preparing dataset for hyperparameter tuning.')
430
+ input_dataset = self.get_dataset()
431
+
432
+ # retrieve checkpoint
433
+ checkpoint = None
434
+ if self.params['checkpoint']:
435
+ self.run.log_message('Retrieving checkpoint.')
436
+ checkpoint = self.get_model(self.params['checkpoint'])
437
+
438
+ # train dataset
439
+ self.run.log_message('Starting training for hyperparameter tuning.')
440
+
441
+ # Save num_samples to TrainRun for logging
442
+ self.run.num_samples = self.params['tune_config']['num_samples']
443
+
444
+ entrypoint = self.entrypoint
445
+ if not self._tune_override_exists():
446
+ # entrypoint must be train entrypoint
447
+ def _tune(param_space, run, dataset, checkpoint=None, **kwargs):
448
+ return entrypoint(run, dataset, param_space, checkpoint, **kwargs)
449
+
450
+ entrypoint = _tune
451
+
452
+ trainable = tune.with_parameters(entrypoint, run=self.run, dataset=input_dataset, checkpoint=checkpoint)
453
+
454
+ tune_config = self.params['tune_config']
455
+
456
+ # Extract search_alg and scheduler as separate objects to avoid JSON serialization issues
457
+ search_alg = self.convert_tune_search_alg(tune_config)
458
+ scheduler = self.convert_tune_scheduler(tune_config)
459
+
460
+ # Create a copy of tune_config without non-serializable objects
461
+ tune_config_dict = {
462
+ 'mode': tune_config.get('mode'),
463
+ 'metric': tune_config.get('metric'),
464
+ 'num_samples': tune_config.get('num_samples', 1),
465
+ 'max_concurrent_trials': tune_config.get('max_concurrent_trials'),
466
+ }
467
+
468
+ # Add search_alg and scheduler to tune_config_dict only if they exist
469
+ if search_alg is not None:
470
+ tune_config_dict['search_alg'] = search_alg
471
+ if scheduler is not None:
472
+ tune_config_dict['scheduler'] = scheduler
473
+
474
+ hyperparameters = self.params['hyperparameters']
475
+ param_space = self.convert_tune_params(hyperparameters)
476
+ temp_path = tempfile.TemporaryDirectory()
477
+
478
+ tuner = tune.Tuner(
479
+ tune.with_resources(trainable, resources=self.tune_resources),
480
+ tune_config=tune.TuneConfig(**tune_config_dict),
481
+ run_config=tune.RunConfig(
482
+ name=f'synapse_tune_hpo_{self.job_id}',
483
+ log_to_file=('stdout.log', 'stderr.log'),
484
+ storage_path=temp_path.name,
485
+ ),
486
+ param_space=param_space,
487
+ )
488
+ result = tuner.fit()
489
+
490
+ best_result = result.get_best_result()
491
+
492
+ # upload model_data
493
+ self.run.log_message('Registering best model data.')
494
+ self.run.set_progress(0, 1, category='model_upload')
495
+ self.create_model_from_result(best_result)
496
+ self.run.set_progress(1, 1, category='model_upload')
497
+
498
+ self.run.end_log()
499
+
500
+ return {'best_result': best_result.config}
501
+
110
502
  def get_dataset(self):
111
503
  client = self.run.client
112
504
  assert bool(client)
@@ -157,3 +549,181 @@ class TrainAction(Action):
157
549
  'configuration': configuration,
158
550
  **params,
159
551
  })
552
+
553
+ @property
554
+ def tune_resources(self):
555
+ resources = {}
556
+ for option in ['num_cpus', 'num_gpus']:
557
+ option_value = self.params.get(option)
558
+ if option_value:
559
+ # Remove the 'num_' prefix and trailing s from the option name
560
+ resources[(lambda s: s[4:-1])(option)] = option_value
561
+ return resources
562
+
563
+ def create_model_from_result(self, result):
564
+ params = copy.deepcopy(self.params)
565
+ configuration_fields = ['hyperparameters']
566
+ configuration = {field: params.pop(field) for field in configuration_fields}
567
+
568
+ with tempfile.TemporaryDirectory() as temp_path:
569
+ archive_path = Path(temp_path, 'archive.zip')
570
+
571
+ # Archive tune results
572
+ # https://docs.ray.io/en/latest/tune/tutorials/tune_get_data_in_and_out.html#getting-data-out-of-tune-using-checkpoints-other-artifacts
573
+ archive(result.path, archive_path)
574
+
575
+ return self.client.create_model({
576
+ 'plugin': self.plugin_release.plugin,
577
+ 'version': self.plugin_release.version,
578
+ 'file': str(archive_path),
579
+ 'configuration': configuration,
580
+ **params,
581
+ })
582
+
583
+ @staticmethod
584
+ def convert_tune_scheduler(tune_config):
585
+ """
586
+ Convert YAML hyperparameter configuration to a Ray Tune scheduler.
587
+
588
+ Args:
589
+ tune_config (dict): Hyperparameter configuration.
590
+
591
+ Returns:
592
+ object: Ray Tune scheduler instance.
593
+
594
+ Supported schedulers:
595
+ - 'fifo': FIFOScheduler (default)
596
+ - 'hyperband': HyperBandScheduler
597
+ """
598
+
599
+ from ray.tune.schedulers import (
600
+ ASHAScheduler,
601
+ FIFOScheduler,
602
+ HyperBandScheduler,
603
+ MedianStoppingRule,
604
+ PopulationBasedTraining,
605
+ )
606
+
607
+ if tune_config.get('scheduler') is None:
608
+ return None
609
+
610
+ scheduler_map = {
611
+ 'fifo': FIFOScheduler,
612
+ 'asha': ASHAScheduler,
613
+ 'hyperband': HyperBandScheduler,
614
+ 'pbt': PopulationBasedTraining,
615
+ 'median': MedianStoppingRule,
616
+ }
617
+
618
+ scheduler_type = tune_config['scheduler'].get('name', 'fifo').lower()
619
+ scheduler_class = scheduler_map.get(scheduler_type, FIFOScheduler)
620
+
621
+ # 옵션이 있는 경우 전달하고, 없으면 기본 생성자 호출
622
+ options = tune_config['scheduler'].get('options')
623
+
624
+ # options가 None이거나 빈 딕셔너리가 아닌 경우에만 전달
625
+ scheduler = scheduler_class(**options) if options else scheduler_class()
626
+
627
+ return scheduler
628
+
629
+ @staticmethod
630
+ def convert_tune_search_alg(tune_config):
631
+ """
632
+ Convert YAML hyperparameter configuration to Ray Tune search algorithm.
633
+
634
+ Args:
635
+ tune_config (dict): Hyperparameter configuration.
636
+
637
+ Returns:
638
+ object: Ray Tune search algorithm instance or None
639
+
640
+ Supported search algorithms:
641
+ - 'bayesoptsearch': Bayesian optimization
642
+ - 'hyperoptsearch': Tree-structured Parzen Estimator
643
+ - 'basicvariantgenerator': Random search (default)
644
+ """
645
+
646
+ if tune_config.get('search_alg') is None:
647
+ return None
648
+
649
+ search_alg_name = tune_config['search_alg']['name'].lower()
650
+ metric = tune_config['metric']
651
+ mode = tune_config['mode']
652
+ points_to_evaluate = tune_config['search_alg'].get('points_to_evaluate', None)
653
+
654
+ if search_alg_name == 'axsearch':
655
+ from ray.tune.search.ax import AxSearch
656
+
657
+ search_alg = AxSearch(metric=metric, mode=mode)
658
+ elif search_alg_name == 'bayesoptsearch':
659
+ from ray.tune.search.bayesopt import BayesOptSearch
660
+
661
+ search_alg = BayesOptSearch(metric=metric, mode=mode)
662
+ elif search_alg_name == 'hyperoptsearch':
663
+ from ray.tune.search.hyperopt import HyperOptSearch
664
+
665
+ search_alg = HyperOptSearch(metric=metric, mode=mode)
666
+ elif search_alg_name == 'optunasearch':
667
+ from ray.tune.search.optuna import OptunaSearch
668
+
669
+ search_alg = OptunaSearch(metric=metric, mode=mode)
670
+ elif search_alg_name == 'basicvariantgenerator':
671
+ from ray.tune.search.basic_variant import BasicVariantGenerator
672
+
673
+ search_alg = BasicVariantGenerator(points_to_evaluate=points_to_evaluate)
674
+ else:
675
+ raise ValueError(
676
+ f'Unsupported search algorithm: {search_alg_name}. '
677
+ f'Supported algorithms are: bayesoptsearch, hyperoptsearch, basicvariantgenerator'
678
+ )
679
+
680
+ return search_alg
681
+
682
+ @staticmethod
683
+ def convert_tune_params(param_list):
684
+ """
685
+ Convert YAML hyperparameter configuration to Ray Tune parameter dictionary.
686
+
687
+ Args:
688
+ param_list (list): List of hyperparameter configurations.
689
+
690
+ Returns:
691
+ dict: Ray Tune parameter dictionary
692
+ """
693
+ from ray import tune
694
+
695
+ param_handlers = {
696
+ 'uniform': lambda p: tune.uniform(p['min'], p['max']),
697
+ 'quniform': lambda p: tune.quniform(p['min'], p['max']),
698
+ 'loguniform': lambda p: tune.loguniform(p['min'], p['max'], p['base']),
699
+ 'qloguniform': lambda p: tune.qloguniform(p['min'], p['max'], p['base']),
700
+ 'randn': lambda p: tune.randn(p['mean'], p['sd']),
701
+ 'qrandn': lambda p: tune.qrandn(p['mean'], p['sd']),
702
+ 'randint': lambda p: tune.randint(p['min'], p['max']),
703
+ 'qrandint': lambda p: tune.qrandint(p['min'], p['max']),
704
+ 'lograndint': lambda p: tune.lograndint(p['min'], p['max'], p['base']),
705
+ 'qlograndint': lambda p: tune.qlograndint(p['min'], p['max'], p['base']),
706
+ 'choice': lambda p: tune.choice(p['options']),
707
+ 'grid_search': lambda p: tune.grid_search(p['options']),
708
+ }
709
+
710
+ param_space = {}
711
+
712
+ for param in param_list:
713
+ name = param['name']
714
+ param_type = param['type']
715
+
716
+ if param_type in param_handlers:
717
+ param_space[name] = param_handlers[param_type](param)
718
+ else:
719
+ raise ValueError(f'Unknown parameter type: {param_type}')
720
+
721
+ return param_space
722
+
723
+ @staticmethod
724
+ def _tune_override_exists(module_path='plugin.tune') -> bool:
725
+ try:
726
+ import_string(module_path)
727
+ return True
728
+ except ImportError:
729
+ return False