mlcflow 1.3.2__tar.gz → 1.3.3__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 (58) hide show
  1. {mlcflow-1.3.2 → mlcflow-1.3.3}/PKG-INFO +5 -5
  2. {mlcflow-1.3.2 → mlcflow-1.3.3}/README.md +4 -4
  3. mlcflow-1.3.3/VERSION +1 -0
  4. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/script/lint.py +0 -2
  5. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/script/meta_schema.py +0 -1
  6. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/script/module.py +34 -15
  7. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/script/remote_run.py +9 -2
  8. mlcflow-1.3.3/automation/script/slurm_run.py +284 -0
  9. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlc/main.py +49 -3
  10. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlc/meta_schema.py +0 -1
  11. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlc/script_action.py +173 -29
  12. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlcflow.egg-info/PKG-INFO +5 -5
  13. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlcflow.egg-info/SOURCES.txt +2 -0
  14. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlcflow.egg-info/entry_points.txt +7 -0
  15. {mlcflow-1.3.2 → mlcflow-1.3.3}/pyproject.toml +7 -0
  16. mlcflow-1.3.3/tests/test_slurm_run.py +158 -0
  17. mlcflow-1.3.2/VERSION +0 -1
  18. {mlcflow-1.3.2 → mlcflow-1.3.3}/LICENSE.md +0 -0
  19. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/COPYRIGHT.txt +0 -0
  20. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/__init__.py +0 -0
  21. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/script/README.md +0 -0
  22. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/script/__init__.py +0 -0
  23. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/script/apptainer.py +0 -0
  24. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/script/cache_utils.py +0 -0
  25. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/script/compat.py +0 -0
  26. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/script/doc.py +0 -0
  27. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/script/docker.py +0 -0
  28. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/script/docker_utils.py +0 -0
  29. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/script/experiment.py +0 -0
  30. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/script/help.py +0 -0
  31. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/script/meta.json +0 -0
  32. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/script/script_utils.py +0 -0
  33. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/script/validate.py +0 -0
  34. {mlcflow-1.3.2 → mlcflow-1.3.3}/automation/utils.py +0 -0
  35. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlc/__init__.py +0 -0
  36. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlc/__main__.py +0 -0
  37. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlc/action.py +0 -0
  38. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlc/action_factory.py +0 -0
  39. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlc/cache_action.py +0 -0
  40. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlc/cfg_action.py +0 -0
  41. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlc/error_codes.py +0 -0
  42. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlc/experiment_action.py +0 -0
  43. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlc/index.py +0 -0
  44. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlc/item.py +0 -0
  45. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlc/logger.py +0 -0
  46. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlc/repo.py +0 -0
  47. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlc/repo_action.py +0 -0
  48. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlc/utils.py +0 -0
  49. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlcflow.egg-info/dependency_links.txt +0 -0
  50. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlcflow.egg-info/requires.txt +0 -0
  51. {mlcflow-1.3.2 → mlcflow-1.3.3}/mlcflow.egg-info/top_level.txt +0 -0
  52. {mlcflow-1.3.2 → mlcflow-1.3.3}/setup.cfg +0 -0
  53. {mlcflow-1.3.2 → mlcflow-1.3.3}/tests/test_action_invalid_meta_entries.py +0 -0
  54. {mlcflow-1.3.2 → mlcflow-1.3.3}/tests/test_cache_mark_tmp.py +0 -0
  55. {mlcflow-1.3.2 → mlcflow-1.3.3}/tests/test_mlcflow_unix_installer.py +0 -0
  56. {mlcflow-1.3.2 → mlcflow-1.3.3}/tests/test_repo_action_extra_git_args.py +0 -0
  57. {mlcflow-1.3.2 → mlcflow-1.3.3}/tests/test_script_action_apptainer.py +0 -0
  58. {mlcflow-1.3.2 → mlcflow-1.3.3}/tests/test_unix_installer_venv.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mlcflow
3
- Version: 1.3.2
3
+ Version: 1.3.3
4
4
  Summary: An automation interface tailored for CPU/GPU benchmarking
5
5
  Author-email: MLCommons <systems@mlcommons.org>
6
6
  License:
@@ -206,10 +206,10 @@ Dynamic: license-file
206
206
  [![License](https://img.shields.io/badge/License-Apache%202.0-green)](https://github.com/mlcommons/mlcflow/blob/main/LICENSE.md)
207
207
  [![Downloads](https://static.pepy.tech/badge/mlcflow)](https://pepy.tech/project/mlcflow)
208
208
 
209
- [![MLC core actions test](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml/badge.svg?branch=dev)](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml)
210
- [![MLC script automation features test](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml/badge.svg?branch=dev)](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml)
211
- [![MLPerf inference resnet50](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml/badge.svg?branch=dev)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml)
212
- [![MLPerf inference bert (deepsparse, tf, onnxruntime, pytorch)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-bert.yml/badge.svg?branch=dev)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-bert.yml)
209
+ [![MLC core actions test](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml/badge.svg?branch=main)](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml)
210
+ [![MLC script automation features test](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml/badge.svg?branch=main)](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml)
211
+ [![MLPerf inference resnet50](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml/badge.svg?branch=main)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml)
212
+
213
213
 
214
214
 
215
215
  MLCFlow is a versatile CLI and Python interface developed by MLCommons in collaboration with a dedicated team of volunteers (see [Contributors](https://github.com/mlcommons/mlcflow/blob/main/CONTRIBUTORS.md)). It serves as a streamlined replacement for the [CMind](https://github.com/mlcommons/ck/tree/master/cm) tool, designed to drive the automation workflows of MLPerf benchmarks more efficiently. You can use this tool for any of your workflow automation tasks.
@@ -4,10 +4,10 @@
4
4
  [![License](https://img.shields.io/badge/License-Apache%202.0-green)](https://github.com/mlcommons/mlcflow/blob/main/LICENSE.md)
5
5
  [![Downloads](https://static.pepy.tech/badge/mlcflow)](https://pepy.tech/project/mlcflow)
6
6
 
7
- [![MLC core actions test](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml/badge.svg?branch=dev)](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml)
8
- [![MLC script automation features test](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml/badge.svg?branch=dev)](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml)
9
- [![MLPerf inference resnet50](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml/badge.svg?branch=dev)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml)
10
- [![MLPerf inference bert (deepsparse, tf, onnxruntime, pytorch)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-bert.yml/badge.svg?branch=dev)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-bert.yml)
7
+ [![MLC core actions test](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml/badge.svg?branch=main)](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml)
8
+ [![MLC script automation features test](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml/badge.svg?branch=main)](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml)
9
+ [![MLPerf inference resnet50](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml/badge.svg?branch=main)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml)
10
+
11
11
 
12
12
 
13
13
  MLCFlow is a versatile CLI and Python interface developed by MLCommons in collaboration with a dedicated team of volunteers (see [Contributors](https://github.com/mlcommons/mlcflow/blob/main/CONTRIBUTORS.md)). It serves as a streamlined replacement for the [CMind](https://github.com/mlcommons/ck/tree/master/cm) tool, designed to drive the automation workflows of MLPerf benchmarks more efficiently. You can use this tool for any of your workflow automation tasks.
mlcflow-1.3.3/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.3.3
@@ -98,7 +98,6 @@ TOP_LEVEL_KEY_ORDER = [
98
98
  "tags",
99
99
  "tags_help",
100
100
  "private",
101
- "min_mlc_version",
102
101
  "mlc_compat",
103
102
 
104
103
  # Cache
@@ -174,7 +173,6 @@ SECTION_GROUPS = [
174
173
  "tags",
175
174
  "tags_help",
176
175
  "private",
177
- "min_mlc_version",
178
176
  "mlc_compat"]),
179
177
  ("# Cache",
180
178
  ["cache",
@@ -40,7 +40,6 @@ TOP_LEVEL_SCHEMA = {
40
40
  "sort": INT,
41
41
  "category_sort": INT,
42
42
  "private": BOOL,
43
- "min_mlc_version": STR,
44
43
 
45
44
  # Environment
46
45
  "env": DICT, # dict[str, str]
@@ -611,9 +611,8 @@ class ScriptAutomation(Automation):
611
611
  meta = script_item.meta
612
612
  path = script_item.path
613
613
 
614
- # Check mlcflow version requirements
615
- # mlc_compat (new): multi-entry, per-entry messages, warn or block
616
- # min_mlc_version (legacy): single threshold, always blocks
614
+ # Check mlcflow version requirements (mlc_compat: multi-entry,
615
+ # per-entry messages, warn or block)
617
616
  script_alias = meta.get('alias', '')
618
617
  try:
619
618
  from script.compat import get_installed_version, check_mlc_compat, format_compat_notice
@@ -635,18 +634,6 @@ class ScriptAutomation(Automation):
635
634
  )}
636
635
  else:
637
636
  self.logger.warning(notice)
638
- else:
639
- # Fall back to legacy min_mlc_version (single threshold, always
640
- # blocks)
641
- min_mlc_version = meta.get('min_mlc_version', '').strip()
642
- if min_mlc_version and current_mlc_version:
643
- if utils.compare_versions(
644
- current_mlc_version, min_mlc_version) < 0:
645
- return {'return': 1, 'error': (
646
- 'This script requires mlcflow >= {} (installed: {}) '
647
- '— please upgrade: pip install --upgrade mlcflow'.format(
648
- min_mlc_version, current_mlc_version)
649
- )}
650
637
  except Exception as e:
651
638
  # Never let a compat-check failure block script execution
652
639
  self.logger.debug('mlc_compat check skipped: {}'.format(e))
@@ -4539,6 +4526,38 @@ pip install mlcflow
4539
4526
  from script.remote_run import remote_run
4540
4527
  return remote_run(self, i)
4541
4528
 
4529
+ ############################################################
4530
+ def slurm_run(self, i):
4531
+ from script.slurm_run import slurm_run
4532
+ return slurm_run(self, i)
4533
+
4534
+ ############################################################
4535
+ def slurm_docker(self, i):
4536
+ from script.slurm_run import slurm_docker
4537
+ return slurm_docker(self, i)
4538
+
4539
+ ############################################################
4540
+ def slurm_apptainer(self, i):
4541
+ from script.slurm_run import slurm_apptainer
4542
+ return slurm_apptainer(self, i)
4543
+
4544
+ ############################################################
4545
+ def slurm_experiment(self, i):
4546
+ from script.slurm_run import slurm_experiment
4547
+ return slurm_experiment(self, i)
4548
+
4549
+ ############################################################
4550
+ def remote_slurm(self, i):
4551
+ from script.remote_run import remote_run
4552
+ i['remote_action'] = 'slurm-run'
4553
+ return remote_run(self, i)
4554
+
4555
+ ############################################################
4556
+ def remote_slurm_experiment(self, i):
4557
+ from script.remote_run import remote_run
4558
+ i['remote_action'] = 'slurm-experiment'
4559
+ return remote_run(self, i)
4560
+
4542
4561
  ##########################################################################
4543
4562
 
4544
4563
  def _available_variations(self, i):
@@ -320,8 +320,15 @@ def regenerate_script_cmd(i):
320
320
  # docker_run_cmd_prefix = i.get('docker_run_cmd_prefix', '')
321
321
 
322
322
  # Regenerate command from dictionary input
323
- run_cmd = 'mlcd' if remote_action == 'docker' else (
324
- 'mlce' if remote_action == 'experiment' else 'mlcr')
323
+ _remote_action_cmd = {
324
+ 'docker': 'mlcd',
325
+ 'experiment': 'mlce',
326
+ 'slurm-run': 'mlcsr',
327
+ 'slurm-experiment': 'mlcse',
328
+ 'slurm-docker': 'mlcsd',
329
+ 'slurm-apptainer': 'mlcsa',
330
+ }
331
+ run_cmd = _remote_action_cmd.get(remote_action, 'mlcr')
325
332
 
326
333
  skip_input_for_fake_run = remote_run_settings.get(
327
334
  'skip_input_for_fake_run', [])
@@ -0,0 +1,284 @@
1
+ import os
2
+ import shlex
3
+ import shutil
4
+ import subprocess
5
+ import copy
6
+ from utils import is_true, prune_input
7
+ from script.script_utils import *
8
+
9
+
10
+ def slurm_run(self_module, i, slurm_action='run'):
11
+ """
12
+ Run MLC scripts on a SLURM cluster node via srun.
13
+
14
+ Args:
15
+ self_module: Reference to the current module for internal calls.
16
+ i: Dictionary containing input parameters for the slurm execution.
17
+ slurm_action: The MLC action to use on the node ('run', 'docker',
18
+ 'apptainer', 'experiment'). Defaults to 'run'.
19
+
20
+ Returns:
21
+ Dictionary with the result of the operation. Keys:
22
+ - 'return': 0 on success, >0 on error.
23
+ - 'error': Error message (if any).
24
+ """
25
+
26
+ # Extract and handle basic inputs
27
+ quiet = i.get('quiet', False)
28
+ logger = self_module.logger
29
+ env = i.get('env', {})
30
+
31
+ # SLURM-specific options with reasonable defaults
32
+ slurm_partition = i.get('slurm_partition', '')
33
+ slurm_nodes = i.get('slurm_nodes', '1')
34
+ slurm_ntasks = i.get('slurm_ntasks', '1')
35
+ slurm_ntasks_per_node = i.get('slurm_ntasks_per_node', '')
36
+ slurm_cpus_per_task = i.get('slurm_cpus_per_task', '')
37
+ slurm_gpus = i.get('slurm_gpus', '')
38
+ slurm_gpus_per_node = i.get('slurm_gpus_per_node', '')
39
+ slurm_gpus_per_task = i.get('slurm_gpus_per_task', '')
40
+ slurm_mem = i.get('slurm_mem', '')
41
+ slurm_mem_per_cpu = i.get('slurm_mem_per_cpu', '')
42
+ slurm_mem_per_gpu = i.get('slurm_mem_per_gpu', '')
43
+ slurm_time = i.get('slurm_time', '')
44
+ slurm_job_name = i.get('slurm_job_name', '')
45
+ slurm_output = i.get('slurm_output', '')
46
+ slurm_error = i.get('slurm_error', '')
47
+ slurm_account = i.get('slurm_account', '')
48
+ slurm_qos = i.get('slurm_qos', '')
49
+ slurm_constraint = i.get('slurm_constraint', '')
50
+ slurm_exclusive = i.get('slurm_exclusive', False)
51
+ slurm_export = i.get('slurm_export', 'ALL')
52
+ slurm_srun_extra_args = i.get('slurm_srun_extra_args', '')
53
+ slurm_python_venv = i.get('slurm_python_venv', 'mlcflow')
54
+ slurm_pull_mlc_repos = i.get('slurm_pull_mlc_repos', False)
55
+ slurm_pre_run_cmds = i.get('slurm_pre_run_cmds', [])
56
+ slurm_post_run_cmds = i.get('slurm_post_run_cmds', [])
57
+
58
+ # Normalize str → list so a single command string doesn't get iterated
59
+ # char-by-char
60
+ if isinstance(slurm_pre_run_cmds, str):
61
+ slurm_pre_run_cmds = [slurm_pre_run_cmds] if slurm_pre_run_cmds else []
62
+ if isinstance(slurm_post_run_cmds, str):
63
+ slurm_post_run_cmds = [
64
+ slurm_post_run_cmds] if slurm_post_run_cmds else []
65
+
66
+ # Check that srun is available before proceeding
67
+ if not shutil.which('srun'):
68
+ return {
69
+ 'return': 1,
70
+ 'error': "srun not found in PATH -- are you on a SLURM login node?"
71
+ }
72
+
73
+ # Validate and normalize integer SLURM parameters
74
+ int_params = {
75
+ 'slurm_nodes': slurm_nodes,
76
+ 'slurm_ntasks': slurm_ntasks,
77
+ 'slurm_ntasks_per_node': slurm_ntasks_per_node,
78
+ 'slurm_cpus_per_task': slurm_cpus_per_task,
79
+ }
80
+ for param_name, param_val in int_params.items():
81
+ if param_val != '':
82
+ try:
83
+ int(param_val)
84
+ except (ValueError, TypeError):
85
+ return {
86
+ 'return': 1,
87
+ 'error': f'Invalid value for {param_name}: {param_val!r} (must be an integer)'
88
+ }
89
+ slurm_nodes = str(int(slurm_nodes)) if slurm_nodes else ''
90
+ slurm_ntasks = str(int(slurm_ntasks)) if slurm_ntasks else ''
91
+ slurm_ntasks_per_node = str(
92
+ int(slurm_ntasks_per_node)) if slurm_ntasks_per_node else ''
93
+ slurm_cpus_per_task = str(
94
+ int(slurm_cpus_per_task)) if slurm_cpus_per_task else ''
95
+
96
+ prune_result = prune_input(
97
+ {'input': i, 'extra_keys_starts_with': ['slurm_']})
98
+ if prune_result['return'] > 0:
99
+ return prune_result
100
+
101
+ run_input = prune_result['new_input']
102
+
103
+ cur_dir = os.getcwd()
104
+
105
+ r = self_module._select_script(i)
106
+ if r['return'] > 0:
107
+ return r
108
+
109
+ script = r['script']
110
+
111
+ meta, script_path = script.meta, script.path
112
+ tags, script_alias, script_uid = meta.get(
113
+ "tags", []), meta.get(
114
+ 'alias', ''), meta.get(
115
+ 'uid', '')
116
+
117
+ r = self_module.update_run_state_for_selected_script_and_variations(
118
+ script, i)
119
+ if r['return'] > 0:
120
+ return r
121
+
122
+ run_state = self_module.run_state
123
+
124
+ env = self_module.env
125
+ state = self_module.state
126
+
127
+ i_copy = copy.deepcopy(i)
128
+ i_copy['run_cmd'] = run_input
129
+ i_copy['slurm_action'] = slurm_action
130
+
131
+ r = regenerate_script_cmd(i_copy)
132
+ if r['return'] > 0:
133
+ return r
134
+
135
+ script_run_cmd = r['run_cmd_string']
136
+
137
+ if quiet:
138
+ script_run_cmd += ' --quiet'
139
+
140
+ # Build the commands to run inside srun
141
+ run_cmds = []
142
+
143
+ # Bootstrap mlcflow on the node
144
+ quoted_venv = shlex.quote(slurm_python_venv)
145
+ run_cmds.append(
146
+ f'curl -sSL https://raw.githubusercontent.com/mlcommons/mlcflow/refs/heads/dev/docs/install/mlcflow_unix_installer.sh | bash -s -- --yes --venv-dir {quoted_venv}')
147
+ run_cmds.append(f'. {quoted_venv}/bin/activate')
148
+
149
+ if is_true(slurm_pull_mlc_repos):
150
+ run_cmds.append('mlc pull repo')
151
+
152
+ # Pre-run commands (after mlcflow is available)
153
+ run_cmds.extend(slurm_pre_run_cmds)
154
+
155
+ # The actual script command
156
+ run_cmds.append(script_run_cmd)
157
+
158
+ # Post-run commands
159
+ run_cmds.extend(slurm_post_run_cmds)
160
+
161
+ # Join all commands with && so failure stops execution
162
+ combined_cmd = ' && '.join(run_cmds)
163
+
164
+ # Build srun command
165
+ srun_args = []
166
+ srun_args.append('srun')
167
+
168
+ if slurm_partition:
169
+ srun_args.extend(['--partition', slurm_partition])
170
+ if slurm_nodes:
171
+ srun_args.extend(['--nodes', str(slurm_nodes)])
172
+ if slurm_ntasks:
173
+ srun_args.extend(['--ntasks', str(slurm_ntasks)])
174
+ if slurm_ntasks_per_node:
175
+ srun_args.extend(['--ntasks-per-node', str(slurm_ntasks_per_node)])
176
+ if slurm_cpus_per_task:
177
+ srun_args.extend(['--cpus-per-task', str(slurm_cpus_per_task)])
178
+ if slurm_gpus:
179
+ srun_args.extend(['--gpus', str(slurm_gpus)])
180
+ if slurm_gpus_per_node:
181
+ srun_args.extend(['--gpus-per-node', str(slurm_gpus_per_node)])
182
+ if slurm_gpus_per_task:
183
+ srun_args.extend(['--gpus-per-task', str(slurm_gpus_per_task)])
184
+ if slurm_mem:
185
+ srun_args.extend(['--mem', str(slurm_mem)])
186
+ if slurm_mem_per_cpu:
187
+ srun_args.extend(['--mem-per-cpu', str(slurm_mem_per_cpu)])
188
+ if slurm_mem_per_gpu:
189
+ srun_args.extend(['--mem-per-gpu', str(slurm_mem_per_gpu)])
190
+ if slurm_time:
191
+ srun_args.extend(['--time', str(slurm_time)])
192
+ if slurm_job_name:
193
+ srun_args.extend(['--job-name', slurm_job_name])
194
+ if slurm_output:
195
+ srun_args.extend(['--output', slurm_output])
196
+ if slurm_error:
197
+ srun_args.extend(['--error', slurm_error])
198
+ if slurm_account:
199
+ srun_args.extend(['--account', slurm_account])
200
+ if slurm_qos:
201
+ srun_args.extend(['--qos', slurm_qos])
202
+ if slurm_constraint:
203
+ srun_args.extend(['--constraint', slurm_constraint])
204
+ if is_true(slurm_exclusive):
205
+ srun_args.append('--exclusive')
206
+ if slurm_export:
207
+ srun_args.extend(['--export', slurm_export])
208
+
209
+ # Append any extra srun arguments the user provided
210
+ if slurm_srun_extra_args:
211
+ srun_args.extend(shlex.split(slurm_srun_extra_args))
212
+
213
+ # Wrap the combined command in bash -c for srun
214
+ srun_args.extend(['bash', '-c', combined_cmd])
215
+
216
+ logger.info(f'Running on SLURM: {shlex.join(srun_args)}')
217
+
218
+ rc = subprocess.call(srun_args)
219
+
220
+ if rc != 0:
221
+ return {'return': 1,
222
+ 'error': f'srun exited with return code {rc}'}
223
+
224
+ return {'return': 0}
225
+
226
+
227
+ def regenerate_script_cmd(i):
228
+ """
229
+ Rebuild the mlcr/mlcd/mlce/mlca command string from the pruned input dict.
230
+ """
231
+
232
+ i_run_cmd = i['run_cmd']
233
+ slurm_action = i.get('slurm_action', 'run')
234
+
235
+ action_to_cmd = {
236
+ 'run': 'mlcr',
237
+ 'docker': 'mlcd',
238
+ 'apptainer': 'mlca',
239
+ 'experiment': 'mlce',
240
+ }
241
+ run_cmd = action_to_cmd.get(slurm_action, 'mlcr')
242
+
243
+ def rebuild_flags(command_dict, prefix):
244
+ command_line = ""
245
+ keys = sorted(command_dict.keys(), key=lambda x: x != "tags")
246
+
247
+ for key in keys:
248
+ full_key = f"{prefix}.{key}" if prefix else key
249
+ value = command_dict[key]
250
+
251
+ if isinstance(value, dict):
252
+ if value:
253
+ command_line += rebuild_flags(value, full_key)
254
+ elif isinstance(value, list):
255
+ if value:
256
+ list_values = ",".join(
257
+ shlex.quote(str(item)) for item in value)
258
+ command_line += f" --{full_key},={list_values}"
259
+ else:
260
+ if full_key in ['s', 'v']:
261
+ command_line += f" -{full_key}"
262
+ else:
263
+ command_line += f" --{full_key}={shlex.quote(str(value))}"
264
+
265
+ return command_line
266
+
267
+ run_cmd += rebuild_flags(i_run_cmd, "")
268
+
269
+ return {'return': 0, 'run_cmd_string': run_cmd}
270
+
271
+
272
+ def slurm_docker(self_module, i):
273
+ """Run an MLC docker script on a SLURM cluster node via srun."""
274
+ return slurm_run(self_module, i, slurm_action='docker')
275
+
276
+
277
+ def slurm_apptainer(self_module, i):
278
+ """Run an MLC apptainer script on a SLURM cluster node via srun."""
279
+ return slurm_run(self_module, i, slurm_action='apptainer')
280
+
281
+
282
+ def slurm_experiment(self_module, i):
283
+ """Run an MLC experiment on a SLURM cluster node via srun."""
284
+ return slurm_run(self_module, i, slurm_action='experiment')
@@ -240,6 +240,34 @@ def mlcrd():
240
240
  mlc_expand_short("remote-docker")
241
241
 
242
242
 
243
+ def mlcsr():
244
+ mlc_expand_short("slurm-run")
245
+
246
+
247
+ def mlcse():
248
+ mlc_expand_short("slurm-experiment")
249
+
250
+
251
+ def mlcsd():
252
+ mlc_expand_short("slurm-docker")
253
+
254
+
255
+ def mlcsa():
256
+ mlc_expand_short("slurm-apptainer")
257
+
258
+
259
+ def mlcrs():
260
+ mlc_expand_short("remote-slurm")
261
+
262
+
263
+ def mlcres():
264
+ mlc_expand_short("remote-slurm-experiment")
265
+
266
+
267
+ def mlcrse():
268
+ mlc_expand_short("remote-slurm-experiment")
269
+
270
+
243
271
  def mlce():
244
272
  mlc_expand_short("experiment")
245
273
 
@@ -374,7 +402,10 @@ def build_parser(pre_args):
374
402
  # Script-only
375
403
  for action in ['docker', 'docker-run', 'apptainer', 'apptainer-run',
376
404
  'experiment', 'remote-run', 'remote-experiment',
377
- 'remote-docker', 'doc', 'lint']:
405
+ 'remote-docker', 'remote-slurm', 'remote-slurm-experiment',
406
+ 'slurm-run',
407
+ 'slurm-docker', 'slurm-apptainer', 'slurm-experiment',
408
+ 'doc', 'lint']:
378
409
  p = subparsers.add_parser(action, add_help=False)
379
410
  p.add_argument('target', choices=['script', 'run'])
380
411
  p.add_argument(
@@ -416,7 +447,10 @@ def build_run_args(args):
416
447
 
417
448
  if args.command in ['docker', 'docker-run', 'apptainer', 'apptainer-run',
418
449
  'experiment', 'remote-run', 'remote-experiment',
419
- 'remote-docker', 'doc', 'lint'] and args.target == "run":
450
+ 'remote-docker', 'remote-slurm', 'remote-slurm-experiment',
451
+ 'slurm-run',
452
+ 'slurm-docker', 'slurm-apptainer', 'slurm-experiment',
453
+ 'doc', 'lint'] and args.target == "run":
420
454
  # run_args['target'] = 'script' #dont modify this as script might have
421
455
  # target as in input
422
456
  args.target = "script"
@@ -552,7 +586,19 @@ def main():
552
586
  if not pre_args.action and not pre_args.target:
553
587
  help_text += main.__doc__
554
588
  elif pre_args.action and not pre_args.target:
555
- if pre_args.action not in ['script', 'cache', 'repo']:
589
+ script_only_actions = {
590
+ a.replace("-", "_")
591
+ for a in [
592
+ 'docker', 'docker-run', 'apptainer', 'apptainer-run',
593
+ 'experiment', 'remote-run', 'remote-experiment',
594
+ 'remote-docker', 'remote-slurm', 'remote-slurm-experiment',
595
+ 'slurm-run', 'slurm-docker', 'slurm-apptainer', 'slurm-experiment',
596
+ 'doc', 'lint',
597
+ ]
598
+ }
599
+ if pre_args.action in script_only_actions:
600
+ pre_args.target = 'script'
601
+ elif pre_args.action not in ['script', 'cache', 'repo']:
556
602
  logger.error(f"Invalid target {pre_args.action}")
557
603
  raise Exception(f"""Invalid target {pre_args.action}""")
558
604
  else:
@@ -40,7 +40,6 @@ TOP_LEVEL_SCHEMA = {
40
40
  "sort": INT,
41
41
  "category_sort": INT,
42
42
  "private": BOOL,
43
- "min_mlc_version": STR,
44
43
 
45
44
  # Environment
46
45
  "env": DICT, # dict[str, str]
@@ -300,36 +300,17 @@ Main Script Meta:""")
300
300
  self, module_path)
301
301
 
302
302
  try:
303
- if function_name == "run":
304
- result = automation_instance.run(
305
- run_args) # Pass args to the run method
306
- elif function_name == "docker":
307
- result = automation_instance.docker(
308
- run_args) # Pass args to the run method
309
- elif function_name == "apptainer":
310
- result = automation_instance.apptainer(
311
- run_args) # Pass args to the apptainer method
312
- elif function_name == "test":
313
- result = automation_instance.test(
314
- run_args) # Pass args to the run method
315
- elif function_name == "experiment":
316
- result = automation_instance.experiment(
317
- run_args) # Pass args to the experiment method
318
- elif function_name == "remote_run":
319
- result = automation_instance.remote_run(
320
- run_args) # Pass args to the experiment method
321
- elif function_name == "help":
322
- result = automation_instance.help(
323
- run_args) # Pass args to the help method
324
- elif function_name == "doc":
325
- result = automation_instance.doc(
326
- run_args) # Pass args to the doc method
327
- elif function_name == "lint":
328
- result = automation_instance.lint(
329
- run_args) # Pass args to the lint method
330
- else:
303
+ method = getattr(automation_instance, function_name, None)
304
+ if method is None:
331
305
  return {
332
- 'return': 1, 'error': f'Function {function_name} is not supported'}
306
+ 'return': 1,
307
+ 'error': (
308
+ f"Action '{function_name}' is not supported by the loaded "
309
+ f"automation engine at '{module_path}'. "
310
+ "You may need a newer version of mlcflow."
311
+ )
312
+ }
313
+ result = method(run_args)
333
314
  except ScriptExecutionError:
334
315
  raise
335
316
  except Exception as exc:
@@ -734,6 +715,169 @@ Main Script Meta:""")
734
715
  run_args["remote_action"] = "docker"
735
716
  return self.call_script_module_function("remote_run", run_args)
736
717
 
718
+ def slurm_run(self, run_args):
719
+ """
720
+ ################################################################################
721
+ Target: Script
722
+ Action: slurm-run
723
+ ################################################################################
724
+
725
+ The `slurm-run` action runs an MLC script on a SLURM cluster node via srun.
726
+
727
+ Flags Available:
728
+
729
+ 1. --slurm_partition:
730
+ SLURM partition to submit to
731
+ 2. --slurm_nodes:
732
+ Number of nodes (default: 1)
733
+ 3. --slurm_ntasks:
734
+ Number of tasks (default: 1)
735
+ 4. --slurm_ntasks_per_node:
736
+ Number of tasks per node
737
+ 5. --slurm_cpus_per_task:
738
+ Number of CPUs per task
739
+ 6. --slurm_gpus:
740
+ Total number of GPUs
741
+ 7. --slurm_gpus_per_node:
742
+ Number of GPUs per node
743
+ 8. --slurm_gpus_per_task:
744
+ Number of GPUs per task
745
+ 9. --slurm_mem:
746
+ Total memory (e.g., 16G)
747
+ 10. --slurm_mem_per_cpu:
748
+ Memory per CPU (e.g., 4G)
749
+ 11. --slurm_mem_per_gpu:
750
+ Memory per GPU (e.g., 8G)
751
+ 12. --slurm_time:
752
+ Time limit (e.g., 01:00:00)
753
+ 13. --slurm_job_name:
754
+ Job name
755
+ 14. --slurm_output:
756
+ Output file pattern (e.g., slurm-%j.out)
757
+ 15. --slurm_error:
758
+ Error file pattern (e.g., slurm-%j.err)
759
+ 16. --slurm_account:
760
+ Account to charge
761
+ 17. --slurm_qos:
762
+ Quality of service
763
+ 18. --slurm_constraint:
764
+ Node feature constraint
765
+ 19. --slurm_exclusive:
766
+ Request exclusive node access (default: False)
767
+ 20. --slurm_export:
768
+ Environment export mode (default: ALL)
769
+ 21. --slurm_srun_extra_args:
770
+ Additional arguments to pass directly to srun
771
+ 22. --slurm_python_venv:
772
+ Python virtual environment name on the node (default: mlcflow)
773
+ 23. --slurm_pull_mlc_repos:
774
+ Pull MLC repos on the node before running
775
+ 24. --slurm_pre_run_cmds:
776
+ Commands to run on the node before the main script
777
+ 25. --slurm_post_run_cmds:
778
+ Commands to run on the node after the main script
779
+
780
+ Example Command:
781
+
782
+ mlc slurm-run script --tags=detect,os
783
+ mlcsr detect,os --slurm_partition=gpu --slurm_gpus=1 --slurm_time=01:00:00
784
+
785
+ """
786
+ return self.call_script_module_function("slurm_run", run_args)
787
+
788
+ def slurm_docker(self, run_args):
789
+ """
790
+ ################################################################################
791
+ Target: Script
792
+ Action: slurm-docker
793
+ ################################################################################
794
+
795
+ The `slurm-docker` action runs an MLC docker script on a SLURM cluster node
796
+ via srun. Accepts the same --slurm_* flags as slurm-run.
797
+
798
+ Example Command:
799
+
800
+ mlc slurm-docker script --tags=detect,os
801
+ mlcsd detect,os --slurm_partition=gpu --slurm_gpus=1
802
+
803
+ """
804
+ return self.call_script_module_function("slurm_docker", run_args)
805
+
806
+ def slurm_apptainer(self, run_args):
807
+ """
808
+ ################################################################################
809
+ Target: Script
810
+ Action: slurm-apptainer
811
+ ################################################################################
812
+
813
+ The `slurm-apptainer` action runs an MLC apptainer script on a SLURM cluster
814
+ node via srun. Accepts the same --slurm_* flags as slurm-run.
815
+
816
+ Example Command:
817
+
818
+ mlc slurm-apptainer script --tags=detect,os
819
+ mlcsa detect,os --slurm_partition=gpu --slurm_gpus=1
820
+
821
+ """
822
+ return self.call_script_module_function("slurm_apptainer", run_args)
823
+
824
+ def slurm_experiment(self, run_args):
825
+ """
826
+ ################################################################################
827
+ Target: Script
828
+ Action: slurm-experiment
829
+ ################################################################################
830
+
831
+ The `slurm-experiment` action runs an MLC experiment on a SLURM cluster node
832
+ via srun. Accepts the same --slurm_* flags as slurm-run.
833
+
834
+ Example Command:
835
+
836
+ mlc slurm-experiment script --tags=detect,os
837
+ mlcse detect,os --slurm_partition=gpu
838
+
839
+ """
840
+ return self.call_script_module_function("slurm_experiment", run_args)
841
+
842
+ def remote_slurm(self, run_args):
843
+ """
844
+ ################################################################################
845
+ Target: Script
846
+ Action: remote-slurm
847
+ ################################################################################
848
+
849
+ The `remote-slurm` action connects to a remote machine via SSH and runs an
850
+ MLC slurm-run script there. Accepts all --remote_* flags from remote-run as
851
+ well as all --slurm_* flags from slurm-run.
852
+
853
+ Example Command:
854
+
855
+ mlc remote-slurm script --tags=detect,os --remote_host=mycluster.example.com
856
+ mlcrs detect,os --remote_host=mycluster.example.com --slurm_partition=gpu
857
+
858
+ """
859
+ return self.call_script_module_function("remote_slurm", run_args)
860
+
861
+ def remote_slurm_experiment(self, run_args):
862
+ """
863
+ ################################################################################
864
+ Target: Script
865
+ Action: remote-slurm-experiment
866
+ ################################################################################
867
+
868
+ The `remote-slurm-experiment` action connects to a remote machine via SSH
869
+ and runs an MLC slurm-experiment script there. Accepts all --remote_* flags
870
+ from remote-run as well as all --slurm_* flags from slurm-run.
871
+
872
+ Example Command:
873
+
874
+ mlc remote-slurm-experiment script --tags=detect,os --remote_host=mycluster.example.com
875
+ mlcres detect,os --remote_host=mycluster.example.com --slurm_partition=gpu
876
+
877
+ """
878
+ return self.call_script_module_function(
879
+ "remote_slurm_experiment", run_args)
880
+
737
881
 
738
882
  class ScriptExecutionError(Exception):
739
883
  def __init__(self, message, script_name=None, repo_alias=None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mlcflow
3
- Version: 1.3.2
3
+ Version: 1.3.3
4
4
  Summary: An automation interface tailored for CPU/GPU benchmarking
5
5
  Author-email: MLCommons <systems@mlcommons.org>
6
6
  License:
@@ -206,10 +206,10 @@ Dynamic: license-file
206
206
  [![License](https://img.shields.io/badge/License-Apache%202.0-green)](https://github.com/mlcommons/mlcflow/blob/main/LICENSE.md)
207
207
  [![Downloads](https://static.pepy.tech/badge/mlcflow)](https://pepy.tech/project/mlcflow)
208
208
 
209
- [![MLC core actions test](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml/badge.svg?branch=dev)](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml)
210
- [![MLC script automation features test](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml/badge.svg?branch=dev)](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml)
211
- [![MLPerf inference resnet50](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml/badge.svg?branch=dev)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml)
212
- [![MLPerf inference bert (deepsparse, tf, onnxruntime, pytorch)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-bert.yml/badge.svg?branch=dev)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-bert.yml)
209
+ [![MLC core actions test](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml/badge.svg?branch=main)](https://github.com/mlcommons/mlcflow/actions/workflows/test-mlc-core-actions.yaml)
210
+ [![MLC script automation features test](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml/badge.svg?branch=main)](https://github.com/mlcommons/mlperf-automations/actions/workflows/test-mlc-script-features.yml)
211
+ [![MLPerf inference resnet50](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml/badge.svg?branch=main)](https://github.com/mlcommons/mlcflow/actions/workflows/mlperf-inference-resnet50.yml)
212
+
213
213
 
214
214
 
215
215
  MLCFlow is a versatile CLI and Python interface developed by MLCommons in collaboration with a dedicated team of volunteers (see [Contributors](https://github.com/mlcommons/mlcflow/blob/main/CONTRIBUTORS.md)). It serves as a streamlined replacement for the [CMind](https://github.com/mlcommons/ck/tree/master/cm) tool, designed to drive the automation workflows of MLPerf benchmarks more efficiently. You can use this tool for any of your workflow automation tasks.
@@ -21,6 +21,7 @@ automation/script/meta_schema.py
21
21
  automation/script/module.py
22
22
  automation/script/remote_run.py
23
23
  automation/script/script_utils.py
24
+ automation/script/slurm_run.py
24
25
  automation/script/validate.py
25
26
  mlc/__init__.py
26
27
  mlc/__main__.py
@@ -50,4 +51,5 @@ tests/test_cache_mark_tmp.py
50
51
  tests/test_mlcflow_unix_installer.py
51
52
  tests/test_repo_action_extra_git_args.py
52
53
  tests/test_script_action_apptainer.py
54
+ tests/test_slurm_run.py
53
55
  tests/test_unix_installer_venv.py
@@ -8,5 +8,12 @@ mlcp = mlc.main:mlcp
8
8
  mlcr = mlc.main:mlcr
9
9
  mlcrd = mlc.main:mlcrd
10
10
  mlcre = mlc.main:mlcre
11
+ mlcres = mlc.main:mlcres
11
12
  mlcrr = mlc.main:mlcrr
13
+ mlcrs = mlc.main:mlcrs
14
+ mlcrse = mlc.main:mlcrse
15
+ mlcsa = mlc.main:mlcsa
16
+ mlcsd = mlc.main:mlcsd
17
+ mlcse = mlc.main:mlcse
18
+ mlcsr = mlc.main:mlcsr
12
19
  mlct = mlc.main:mlct
@@ -59,3 +59,10 @@ mlct = "mlc.main:mlct"
59
59
  mlcp = "mlc.main:mlcp"
60
60
  mlcre = "mlc.main:mlcre"
61
61
  mlcrd = "mlc.main:mlcrd"
62
+ mlcsr = "mlc.main:mlcsr"
63
+ mlcse = "mlc.main:mlcse"
64
+ mlcsd = "mlc.main:mlcsd"
65
+ mlcsa = "mlc.main:mlcsa"
66
+ mlcrs = "mlc.main:mlcrs"
67
+ mlcres = "mlc.main:mlcres"
68
+ mlcrse = "mlc.main:mlcrse"
@@ -0,0 +1,158 @@
1
+ """
2
+ Unit tests for slurm_run.py command construction and remote_run.py mapping.
3
+ """
4
+ import sys
5
+ import os
6
+ import unittest
7
+
8
+ # Ensure the automation directory is on sys.path so slurm_run / remote_run
9
+ # can be imported without a full mlcflow install.
10
+ _automation_dir = os.path.join(
11
+ os.path.dirname(__file__), '..', 'automation'
12
+ )
13
+ _automation_dir = os.path.abspath(_automation_dir)
14
+ if _automation_dir not in sys.path:
15
+ sys.path.insert(0, _automation_dir)
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # remote_run.regenerate_script_cmd — remote_action → mlc command mapping
20
+ # ---------------------------------------------------------------------------
21
+ class TestRemoteRunMapping(unittest.TestCase):
22
+ def _regenerate(self, remote_action, run_cmd=None):
23
+ from script.remote_run import regenerate_script_cmd
24
+ i = {
25
+ 'remote_action': remote_action,
26
+ 'run_cmd': run_cmd or {'tags': 'detect,os'},
27
+ 'remote_run_settings': {},
28
+ 'fake_run': False,
29
+ }
30
+ result = regenerate_script_cmd(i)
31
+ self.assertEqual(result['return'], 0)
32
+ return result['run_cmd_string']
33
+
34
+ def test_docker_maps_to_mlcd(self):
35
+ self.assertTrue(self._regenerate('docker').startswith('mlcd'))
36
+
37
+ def test_experiment_maps_to_mlce(self):
38
+ self.assertTrue(self._regenerate('experiment').startswith('mlce'))
39
+
40
+ def test_slurm_run_maps_to_mlcsr(self):
41
+ self.assertTrue(self._regenerate('slurm-run').startswith('mlcsr'))
42
+
43
+ def test_slurm_experiment_maps_to_mlcse(self):
44
+ # Regression test for blocker #1: was falling through to 'mlcr'
45
+ self.assertTrue(self._regenerate(
46
+ 'slurm-experiment').startswith('mlcse'))
47
+
48
+ def test_slurm_docker_maps_to_mlcsd(self):
49
+ self.assertTrue(self._regenerate('slurm-docker').startswith('mlcsd'))
50
+
51
+ def test_slurm_apptainer_maps_to_mlcsa(self):
52
+ self.assertTrue(self._regenerate(
53
+ 'slurm-apptainer').startswith('mlcsa'))
54
+
55
+ def test_unknown_action_defaults_to_mlcr(self):
56
+ self.assertTrue(self._regenerate('unknown-action').startswith('mlcr'))
57
+
58
+ def test_run_action_defaults_to_mlcr(self):
59
+ self.assertTrue(self._regenerate('run').startswith('mlcr'))
60
+
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # slurm_run.regenerate_script_cmd — slurm_action → mlc command mapping
64
+ # ---------------------------------------------------------------------------
65
+ class TestSlurmRunCmdGeneration(unittest.TestCase):
66
+ def _regenerate(self, slurm_action, run_cmd=None):
67
+ from script.slurm_run import regenerate_script_cmd
68
+ i = {
69
+ 'slurm_action': slurm_action,
70
+ 'run_cmd': run_cmd or {'tags': 'detect,os'},
71
+ }
72
+ result = regenerate_script_cmd(i)
73
+ self.assertEqual(result['return'], 0)
74
+ return result['run_cmd_string']
75
+
76
+ def test_run_maps_to_mlcr(self):
77
+ self.assertTrue(self._regenerate('run').startswith('mlcr'))
78
+
79
+ def test_docker_maps_to_mlcd(self):
80
+ self.assertTrue(self._regenerate('docker').startswith('mlcd'))
81
+
82
+ def test_apptainer_maps_to_mlca(self):
83
+ self.assertTrue(self._regenerate('apptainer').startswith('mlca'))
84
+
85
+ def test_experiment_maps_to_mlce(self):
86
+ self.assertTrue(self._regenerate('experiment').startswith('mlce'))
87
+
88
+ def test_unknown_defaults_to_mlcr(self):
89
+ self.assertTrue(self._regenerate('unknown').startswith('mlcr'))
90
+
91
+ def test_tags_included_in_output(self):
92
+ cmd = self._regenerate('run', run_cmd={'tags': 'detect,os'})
93
+ self.assertIn('detect,os', cmd)
94
+
95
+ def test_flag_included_in_output(self):
96
+ cmd = self._regenerate(
97
+ 'run',
98
+ run_cmd={
99
+ 'tags': 'detect,os',
100
+ 'verbose': True})
101
+ self.assertIn('--verbose', cmd)
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # slurm_run string normalization (pre/post run cmds)
106
+ # ---------------------------------------------------------------------------
107
+ class TestSlurmRunInputNormalization(unittest.TestCase):
108
+ """
109
+ Test that str slurm_pre_run_cmds / slurm_post_run_cmds are wrapped in a
110
+ list instead of being iterated character-by-character.
111
+ The easiest way to test this is to call slurm_run with a mock that
112
+ immediately returns, and inspect the srun_args built.
113
+ """
114
+
115
+ def test_pre_run_cmds_str_is_normalized(self):
116
+ """A single str command must not be extended char-by-char."""
117
+ import shutil
118
+ from unittest.mock import patch, MagicMock
119
+
120
+ mock_self = MagicMock()
121
+ mock_self._select_script.return_value = {
122
+ 'return': 0,
123
+ 'script': MagicMock(
124
+ meta={'tags': [], 'alias': 'detect-os', 'uid': '0' * 16},
125
+ path='/fake/path'
126
+ )
127
+ }
128
+ mock_self.update_run_state_for_selected_script_and_variations.return_value = {
129
+ 'return': 0}
130
+ mock_self.run_state = {}
131
+ mock_self.env = {}
132
+ mock_self.state = {}
133
+ mock_self.logger = MagicMock()
134
+
135
+ captured_args = []
136
+
137
+ def fake_call(args):
138
+ captured_args.extend(args)
139
+ return 0
140
+
141
+ with patch('script.slurm_run.shutil.which', return_value='/usr/bin/srun'), \
142
+ patch('script.slurm_run.subprocess.call', side_effect=fake_call):
143
+ from script.slurm_run import slurm_run
144
+ result = slurm_run(mock_self, {
145
+ 'tags': 'detect,os',
146
+ 'slurm_pre_run_cmds': 'module load cuda',
147
+ 'env': {},
148
+ })
149
+
150
+ # The bash -c combined_cmd should contain 'module load cuda' as a
151
+ # complete token, not individual characters.
152
+ bash_c_cmd = captured_args[-1] # last element after 'bash', '-c'
153
+ self.assertIn('module load cuda', bash_c_cmd,
154
+ "Pre-run command string was expanded char-by-char instead of as a whole")
155
+
156
+
157
+ if __name__ == '__main__':
158
+ unittest.main()
mlcflow-1.3.2/VERSION DELETED
@@ -1 +0,0 @@
1
- 1.3.2
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