mrs83 commited on
Commit
5f72bf5
·
unverified ·
1 Parent(s): 3a1c55b

Add MLX-LM support (CPU only) (#4)

Browse files

* Add MLX-LM support (CPU only)
* Fix 401 Client Error: Unauthorized for url: https://huggingface.co/api/whoami-v2
* Add FLOWER_APPS settings (comma-separated values)
* Add CA cert download for approved participants

blossomtune_gradio/config.py CHANGED
@@ -37,3 +37,11 @@ BLOSSOMTUNE_TLS_CERT_PATH = os.getenv(
37
  if os.path.isdir("/data/certs")
38
  else "./data/certs/server.crt",
39
  )
 
 
 
 
 
 
 
 
 
37
  if os.path.isdir("/data/certs")
38
  else "./data/certs/server.crt",
39
  )
40
+
41
+ # Flower Apps
42
+ FLOWER_APPS = os.getenv("FLOWER_APPS", ["flower_apps.quickstart_huggingface"])
43
+ FLOWER_APPS = (
44
+ FLOWER_APPS
45
+ if isinstance(FLOWER_APPS, list)
46
+ else [app.strip() for app in FLOWER_APPS.split(",")]
47
+ )
blossomtune_gradio/federation.py CHANGED
@@ -45,16 +45,16 @@ def check_participant_status(pid_to_check: str, email: str, activation_code: str
45
  # Case 1: New user registration
46
  if result is None:
47
  if activation_code:
48
- return (False, settings.get_text("activation_invalid_md"))
49
  if not util.validate_email(email):
50
- return (False, settings.get_text("invalid_email_md"))
51
 
52
  with sqlite3.connect(cfg.DB_PATH) as conn:
53
  cursor = conn.cursor()
54
  cursor.execute("SELECT COUNT(*) FROM requests WHERE status = 'approved'")
55
  approved_count = cursor.fetchone()[0]
56
  if approved_count >= cfg.MAX_NUM_NODES:
57
- return (False, settings.get_text("federation_full_md"))
58
 
59
  participant_id = generate_participant_id()
60
  new_activation_code = generate_activation_code()
@@ -73,9 +73,9 @@ def check_participant_status(pid_to_check: str, email: str, activation_code: str
73
  0,
74
  ),
75
  )
76
- return (True, settings.get_text("registration_submitted_md"))
77
  else:
78
- return (False, message)
79
 
80
  # Existing user
81
  participant_id, status, partition_id, is_activated, stored_code = result
@@ -88,9 +88,9 @@ def check_participant_status(pid_to_check: str, email: str, activation_code: str
88
  "UPDATE requests SET is_activated = 1 WHERE hf_handle = ?",
89
  (pid_to_check,),
90
  )
91
- return (True, settings.get_text("activation_successful_md"))
92
  else:
93
- return (False, settings.get_text("activation_invalid_md"))
94
  else:
95
  if not activation_code:
96
  return (False, settings.get_text("missing_activation_code_md"))
@@ -111,13 +111,15 @@ def check_participant_status(pid_to_check: str, email: str, activation_code: str
111
  superlink_hostname=superlink_hostname,
112
  num_partitions=num_partitions,
113
  )
114
- return True, connection_string
 
115
  elif status == "pending":
116
  return (False, settings.get_text("status_pending_md"))
117
  else: # Denied
118
  return (
119
  False,
120
  settings.get_text("status_denied_md", participant_id=participant_id),
 
121
  )
122
 
123
 
 
45
  # Case 1: New user registration
46
  if result is None:
47
  if activation_code:
48
+ return (False, settings.get_text("activation_invalid_md"), None)
49
  if not util.validate_email(email):
50
+ return (False, settings.get_text("invalid_email_md"), None)
51
 
52
  with sqlite3.connect(cfg.DB_PATH) as conn:
53
  cursor = conn.cursor()
54
  cursor.execute("SELECT COUNT(*) FROM requests WHERE status = 'approved'")
55
  approved_count = cursor.fetchone()[0]
56
  if approved_count >= cfg.MAX_NUM_NODES:
57
+ return (False, settings.get_text("federation_full_md"), None)
58
 
59
  participant_id = generate_participant_id()
60
  new_activation_code = generate_activation_code()
 
73
  0,
74
  ),
75
  )
76
+ return (True, settings.get_text("registration_submitted_md"), None)
77
  else:
78
+ return (False, message, None)
79
 
80
  # Existing user
81
  participant_id, status, partition_id, is_activated, stored_code = result
 
88
  "UPDATE requests SET is_activated = 1 WHERE hf_handle = ?",
89
  (pid_to_check,),
90
  )
91
+ return (True, settings.get_text("activation_successful_md"), None)
92
  else:
93
+ return (False, settings.get_text("activation_invalid_md"), None)
94
  else:
95
  if not activation_code:
96
  return (False, settings.get_text("missing_activation_code_md"))
 
111
  superlink_hostname=superlink_hostname,
112
  num_partitions=num_partitions,
113
  )
114
+ # TODO: build and provide .blossomfile for download
115
+ return (True, connection_string, cfg.BLOSSOMTUNE_TLS_CERT_PATH)
116
  elif status == "pending":
117
  return (False, settings.get_text("status_pending_md"))
118
  else: # Denied
119
  return (
120
  False,
121
  settings.get_text("status_denied_md", participant_id=participant_id),
122
+ None,
123
  )
124
 
125
 
blossomtune_gradio/gradio_app.py CHANGED
@@ -32,6 +32,7 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Flower Superlink & Runner") as dem
32
  )
33
  check_status_btn = gr.Button("Submit Request / Activate", variant="primary")
34
  request_status_md = components.request_status_md.render()
 
35
  check_status_btn.click(
36
  fn=callbacks.on_check_participant_status,
37
  inputs=[hf_handle_tb, email_tb, activation_code_tb],
 
32
  )
33
  check_status_btn = gr.Button("Submit Request / Activate", variant="primary")
34
  request_status_md = components.request_status_md.render()
35
+ ca_cert_download = components.ca_cert_download.render()
36
  check_status_btn.click(
37
  fn=callbacks.on_check_participant_status,
38
  inputs=[hf_handle_tb, email_tb, activation_code_tb],
blossomtune_gradio/ui/auth.py CHANGED
@@ -1,5 +1,6 @@
1
  import gradio as gr
2
  from huggingface_hub import whoami
 
3
 
4
 
5
  from blossomtune_gradio import config as cfg
@@ -10,7 +11,10 @@ def is_space_owner(profile: gr.OAuthProfile | None, oauth_token: gr.OAuthToken |
10
  if cfg.SPACE_OWNER is None:
11
  return True
12
  if oauth_token:
13
- org_names = [org["name"] for org in whoami(oauth_token.token)["orgs"]]
 
 
 
14
  else:
15
  org_names = []
16
  return profile is not None and (
 
1
  import gradio as gr
2
  from huggingface_hub import whoami
3
+ from requests.exceptions import HTTPError
4
 
5
 
6
  from blossomtune_gradio import config as cfg
 
11
  if cfg.SPACE_OWNER is None:
12
  return True
13
  if oauth_token:
14
+ try:
15
+ org_names = [org["name"] for org in whoami(oauth_token.token)["orgs"]]
16
+ except HTTPError:
17
+ return False
18
  else:
19
  org_names = []
20
  return profile is not None and (
blossomtune_gradio/ui/callbacks.py CHANGED
@@ -203,10 +203,15 @@ def on_check_participant_status(
203
  email_to_add = email.strip()
204
  activation_code_to_check = activation_code.strip()
205
  # The federation module is responsible for getting the correct text from settings
206
- _, message = fed.check_participant_status(
207
  pid_to_check, email_to_add, activation_code_to_check
208
  )
209
- return {components.request_status_md: gr.update(value=message)}
 
 
 
 
 
210
 
211
 
212
  def on_manage_fed_request(participant_id: str, partition_id: str, action: str):
 
203
  email_to_add = email.strip()
204
  activation_code_to_check = activation_code.strip()
205
  # The federation module is responsible for getting the correct text from settings
206
+ _, message, download = fed.check_participant_status(
207
  pid_to_check, email_to_add, activation_code_to_check
208
  )
209
+ return {
210
+ components.request_status_md: gr.update(value=message),
211
+ components.ca_cert_download: gr.update(
212
+ value=download or "", visible=True if download else False
213
+ ),
214
+ }
215
 
216
 
217
  def on_manage_fed_request(participant_id: str, partition_id: str, action: str):
blossomtune_gradio/ui/components.py CHANGED
@@ -1,4 +1,5 @@
1
  import gradio as gr
 
2
 
3
 
4
  # This component's value is updated dynamically by callbacks.
@@ -47,9 +48,8 @@ runner_status_txt = gr.Textbox(
47
  )
48
  runner_toggle_btn = gr.Button("▶️ Start Federated Run", variant="primary")
49
  runner_app_dd = gr.Dropdown(
50
- ["flower_apps.quickstart_huggingface"],
51
  label="Select Runner App",
52
- value="flower_apps.quickstart_huggingface",
53
  render=False,
54
  )
55
  run_id_tb = gr.Textbox(label="Run ID", placeholder="e.g., run_123")
@@ -88,3 +88,6 @@ partition_id_tb = gr.Textbox(
88
  placeholder="Auto-filled on selection...",
89
  render=False,
90
  )
 
 
 
 
1
  import gradio as gr
2
+ from blossomtune_gradio import config as cfg
3
 
4
 
5
  # This component's value is updated dynamically by callbacks.
 
48
  )
49
  runner_toggle_btn = gr.Button("▶️ Start Federated Run", variant="primary")
50
  runner_app_dd = gr.Dropdown(
51
+ cfg.FLOWER_APPS,
52
  label="Select Runner App",
 
53
  render=False,
54
  )
55
  run_id_tb = gr.Textbox(label="Run ID", placeholder="e.g., run_123")
 
88
  placeholder="Auto-filled on selection...",
89
  render=False,
90
  )
91
+ ca_cert_download = gr.File(
92
+ label="Download CA Certificate (ca.crt)", visible=False, render=False
93
+ )
pyproject.toml CHANGED
@@ -70,6 +70,8 @@ convention = "google" # Accepts: "google", "numpy", or "pep257".
70
  dev = [
71
  "cryptography>=44.0.3",
72
  "dnspython>=2.8.0",
 
 
73
  "pytest>=8.4.1",
74
  "pytest-mock>=3.15.1",
75
  ]
 
70
  dev = [
71
  "cryptography>=44.0.3",
72
  "dnspython>=2.8.0",
73
+ "mlx-lm>=0.28.2",
74
+ "mlx[cpu]>=0.29.2",
75
  "pytest>=8.4.1",
76
  "pytest-mock>=3.15.1",
77
  ]
tests/test_federation.py CHANGED
@@ -45,10 +45,11 @@ class TestCheckParticipantStatus:
45
  ):
46
  """Verify successful registration for a new user."""
47
  mock_mail.return_value = (True, "")
48
- success, message = fed.check_participant_status(
49
  "new_user", "new@example.com", ""
50
  )
51
  assert success is True
 
52
  assert message == "mock_registration_submitted_md"
53
 
54
  # Verify the user was added to the database
@@ -58,17 +59,21 @@ class TestCheckParticipantStatus:
58
 
59
  def test_new_user_invalid_email(self, in_memory_db, mock_settings):
60
  """Verify registration fails with an invalid email."""
61
- success, message = fed.check_participant_status("user", "invalid-email", "")
 
 
62
  assert success is False
 
63
  assert message == "mock_invalid_email_md"
64
 
65
  def test_new_user_federation_full(self, in_memory_db, mock_settings, mocker):
66
  """Verify registration fails when the federation is full."""
67
  mocker.patch("blossomtune_gradio.federation.cfg.MAX_NUM_NODES", 0)
68
- success, message = fed.check_participant_status(
69
  "another_user", "another@example.com", ""
70
  )
71
  assert success is False
 
72
  assert message == "mock_federation_full_md"
73
 
74
  def test_user_activation_success(self, in_memory_db, mock_settings):
@@ -81,10 +86,11 @@ class TestCheckParticipantStatus:
81
  )
82
  in_memory_db.commit()
83
 
84
- success, message = fed.check_participant_status(
85
  "test_user", "test@example.com", "ABCDEF"
86
  )
87
  assert success is True
 
88
  assert message == "mock_activation_successful_md"
89
  # Verify the user is now activated
90
  cursor.execute(
@@ -102,10 +108,11 @@ class TestCheckParticipantStatus:
102
  )
103
  in_memory_db.commit()
104
 
105
- success, message = fed.check_participant_status(
106
  "test_user", "test@example.com", "WRONGCODE"
107
  )
108
  assert success is False
 
109
  assert message == "mock_activation_invalid_md"
110
 
111
  def test_status_check_approved(self, in_memory_db, mock_settings):
@@ -125,10 +132,11 @@ class TestCheckParticipantStatus:
125
  ),
126
  )
127
  in_memory_db.commit()
128
- success, message = fed.check_participant_status(
129
  "approved_user", "approved@example.com", "GHIJKL"
130
  )
131
  assert success is True
 
132
  assert "mock_status_approved_md" in message
133
 
134
 
 
45
  ):
46
  """Verify successful registration for a new user."""
47
  mock_mail.return_value = (True, "")
48
+ success, message, download = fed.check_participant_status(
49
  "new_user", "new@example.com", ""
50
  )
51
  assert success is True
52
+ assert download is None
53
  assert message == "mock_registration_submitted_md"
54
 
55
  # Verify the user was added to the database
 
59
 
60
  def test_new_user_invalid_email(self, in_memory_db, mock_settings):
61
  """Verify registration fails with an invalid email."""
62
+ success, message, download = fed.check_participant_status(
63
+ "user", "invalid-email", ""
64
+ )
65
  assert success is False
66
+ assert download is None
67
  assert message == "mock_invalid_email_md"
68
 
69
  def test_new_user_federation_full(self, in_memory_db, mock_settings, mocker):
70
  """Verify registration fails when the federation is full."""
71
  mocker.patch("blossomtune_gradio.federation.cfg.MAX_NUM_NODES", 0)
72
+ success, message, download = fed.check_participant_status(
73
  "another_user", "another@example.com", ""
74
  )
75
  assert success is False
76
+ assert download is None
77
  assert message == "mock_federation_full_md"
78
 
79
  def test_user_activation_success(self, in_memory_db, mock_settings):
 
86
  )
87
  in_memory_db.commit()
88
 
89
+ success, message, download = fed.check_participant_status(
90
  "test_user", "test@example.com", "ABCDEF"
91
  )
92
  assert success is True
93
+ assert download is None
94
  assert message == "mock_activation_successful_md"
95
  # Verify the user is now activated
96
  cursor.execute(
 
108
  )
109
  in_memory_db.commit()
110
 
111
+ success, message, download = fed.check_participant_status(
112
  "test_user", "test@example.com", "WRONGCODE"
113
  )
114
  assert success is False
115
+ assert download is None
116
  assert message == "mock_activation_invalid_md"
117
 
118
  def test_status_check_approved(self, in_memory_db, mock_settings):
 
132
  ),
133
  )
134
  in_memory_db.commit()
135
+ success, message, download = fed.check_participant_status(
136
  "approved_user", "approved@example.com", "GHIJKL"
137
  )
138
  assert success is True
139
+ assert download is not None
140
  assert "mock_status_approved_md" in message
141
 
142
 
uv.lock CHANGED
@@ -243,6 +243,8 @@ dependencies = [
243
  dev = [
244
  { name = "cryptography" },
245
  { name = "dnspython" },
 
 
246
  { name = "pytest" },
247
  { name = "pytest-mock" },
248
  ]
@@ -265,6 +267,8 @@ requires-dist = [
265
  dev = [
266
  { name = "cryptography", specifier = ">=44.0.3" },
267
  { name = "dnspython", specifier = ">=2.8.0" },
 
 
268
  { name = "pytest", specifier = ">=8.4.1" },
269
  { name = "pytest-mock", specifier = ">=3.15.1" },
270
  ]
@@ -1364,6 +1368,68 @@ wheels = [
1364
  { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
1365
  ]
1366
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1367
  [[package]]
1368
  name = "mpmath"
1369
  version = "1.3.0"
@@ -2782,10 +2848,10 @@ dependencies = [
2782
  { name = "typing-extensions", marker = "sys_platform == 'darwin'" },
2783
  ]
2784
  wheels = [
2785
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp311-none-macosx_11_0_arm64.whl" },
2786
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl" },
2787
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl" },
2788
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl" },
2789
  ]
2790
 
2791
  [[package]]
@@ -2807,24 +2873,24 @@ dependencies = [
2807
  { name = "typing-extensions", marker = "sys_platform != 'darwin'" },
2808
  ]
2809
  wheels = [
2810
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-linux_s390x.whl" },
2811
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl" },
2812
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl" },
2813
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-win_amd64.whl" },
2814
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-win_arm64.whl" },
2815
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl" },
2816
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl" },
2817
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl" },
2818
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl" },
2819
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl" },
2820
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl" },
2821
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl" },
2822
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl" },
2823
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_amd64.whl" },
2824
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_arm64.whl" },
2825
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl" },
2826
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl" },
2827
- { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-win_amd64.whl" },
2828
  ]
2829
 
2830
  [[package]]
 
243
  dev = [
244
  { name = "cryptography" },
245
  { name = "dnspython" },
246
+ { name = "mlx", extra = ["cpu"] },
247
+ { name = "mlx-lm" },
248
  { name = "pytest" },
249
  { name = "pytest-mock" },
250
  ]
 
267
  dev = [
268
  { name = "cryptography", specifier = ">=44.0.3" },
269
  { name = "dnspython", specifier = ">=2.8.0" },
270
+ { name = "mlx", extras = ["cpu"], specifier = ">=0.29.2" },
271
+ { name = "mlx-lm", specifier = ">=0.28.2" },
272
  { name = "pytest", specifier = ">=8.4.1" },
273
  { name = "pytest-mock", specifier = ">=3.15.1" },
274
  ]
 
1368
  { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
1369
  ]
1370
 
1371
+ [[package]]
1372
+ name = "mlx"
1373
+ version = "0.29.2"
1374
+ source = { registry = "https://pypi.org/simple" }
1375
+ dependencies = [
1376
+ { name = "mlx-metal", marker = "sys_platform == 'darwin'" },
1377
+ ]
1378
+ wheels = [
1379
+ { url = "https://files.pythonhosted.org/packages/cb/f0/f57349f37cf5dd53f95127e141fc59fc435e4b6bfabba5a84c65de4d3597/mlx-0.29.2-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:e74965369227230374b3e8e8c8d46e209e5221a9b76bbb0fa788617e2c68f73c", size = 547581, upload-time = "2025-09-26T22:21:39.24Z" },
1380
+ { url = "https://files.pythonhosted.org/packages/66/04/e016ca28dc9e0738a2541581420125cfe6bba24466a64420600bdd6fd52c/mlx-0.29.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0f79194eeac78e85b96439d3bbc17aae5aba045a2af083c000b4fbbc501f253e", size = 547581, upload-time = "2025-09-26T22:21:47.706Z" },
1381
+ { url = "https://files.pythonhosted.org/packages/1f/b3/e2595e70ef8d4438dff694857745b0e108911e5b5fb83259dde6e5dc5bd1/mlx-0.29.2-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:33bbbb0fd24895d5ff080bb4d10e3e77017bba675d9a12466c8866eaf9b47854", size = 547578, upload-time = "2025-09-26T22:21:22.041Z" },
1382
+ { url = "https://files.pythonhosted.org/packages/11/8c/5d51543ab128c2dff5e4b44ca799db8db5aa4f4ffc34af6531fd73627b54/mlx-0.29.2-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:32e159f2772be893bec580d2d50c0e6b32ad71a19ded7307bf6c871c8aaa9cf2", size = 651900, upload-time = "2025-09-26T22:26:11.133Z" },
1383
+ { url = "https://files.pythonhosted.org/packages/f3/84/7250237039e91d8e44ca0cf3522f189164844c196f262509afd29ef54710/mlx-0.29.2-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:eec950bf7118ad0865d0fc4686bd85d99bf8463fc717d836a5132e1a08b4f129", size = 548336, upload-time = "2025-09-26T22:21:44.914Z" },
1384
+ { url = "https://files.pythonhosted.org/packages/13/47/428ac8d9b0cb5c136e5ce6c726cfdd55caa5b9497dafb6221acfee18f145/mlx-0.29.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bef7333268d6d02e50a9ac6b10f661b711cd02da4a5e2d7619cf198a7e530308", size = 548334, upload-time = "2025-09-26T22:21:21.41Z" },
1385
+ { url = "https://files.pythonhosted.org/packages/14/f0/7d5d3527ca3fdc664c900b4b822028691739e58c8e8f7975b33df4d3536e/mlx-0.29.2-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:f622fc6a84542a08ad2136e9251822d2c08106e5a1a0bd5d249a2d72bccd6577", size = 548330, upload-time = "2025-09-26T22:21:41.182Z" },
1386
+ { url = "https://files.pythonhosted.org/packages/09/18/e202e0f6232822f6768995cdbf50eda202137bb6547368f6e3993dbee00b/mlx-0.29.2-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:a1aa1aee8e1b6bd1e51361e6b692c70d281b8187b2e859e70ecc11daab306dac", size = 648728, upload-time = "2025-09-26T22:25:49.159Z" },
1387
+ { url = "https://files.pythonhosted.org/packages/a0/9a/91f6f5d031f109fa8c00ba9dd4f7a3fc42e1097a57c26783ce000069c264/mlx-0.29.2-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:05ea54173f4bde11b2c93e673d65d72523f5d850f5112d3874156a6fc74ca591", size = 548297, upload-time = "2025-09-26T22:21:41.991Z" },
1388
+ { url = "https://files.pythonhosted.org/packages/2b/2d/dae7ca0b7fa68c6c1f2b896dfe1b8060647f144d5c5da2d53388e38809b1/mlx-0.29.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:199dd029b5e55b6d94f1ce366d0137824e46e4333891424dd00413c739f50ae9", size = 548305, upload-time = "2025-09-26T22:21:41.083Z" },
1389
+ { url = "https://files.pythonhosted.org/packages/b1/56/f02f5c9e1fc11c020982501a763fa92b497ea50671a587760543987ba8c8/mlx-0.29.2-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:b6dd4e5f227414882b1676d99250d99389228d1bdc14e4e4e88c95d4903810b7", size = 548302, upload-time = "2025-09-26T22:21:30.546Z" },
1390
+ { url = "https://files.pythonhosted.org/packages/4b/b4/b61eeb92c424947675492dec3a411bdbeae307dfd78162d65ab47e8c3b4f/mlx-0.29.2-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:c3b9a9aee13f346d060966472954eebe99d9f1b295c9a237c9a000f1ef9adf2c", size = 648709, upload-time = "2025-09-26T22:26:03.452Z" },
1391
+ ]
1392
+
1393
+ [package.optional-dependencies]
1394
+ cpu = [
1395
+ { name = "mlx-cpu", marker = "sys_platform == 'linux'" },
1396
+ ]
1397
+
1398
+ [[package]]
1399
+ name = "mlx-cpu"
1400
+ version = "0.29.2"
1401
+ source = { registry = "https://pypi.org/simple" }
1402
+ wheels = [
1403
+ { url = "https://files.pythonhosted.org/packages/3d/3a/5f2246e0f68477c141b76da3afaabf0b7b8565eba12451482b3b164df762/mlx_cpu-0.29.2-py3-none-manylinux_2_35_x86_64.whl", hash = "sha256:209f6f587ee4bbff3fd18e5201e150880812cef39e1af2c102d7e8e681f06749", size = 10170075, upload-time = "2025-09-26T22:28:40.523Z" },
1404
+ ]
1405
+
1406
+ [[package]]
1407
+ name = "mlx-lm"
1408
+ version = "0.28.2"
1409
+ source = { registry = "https://pypi.org/simple" }
1410
+ dependencies = [
1411
+ { name = "jinja2" },
1412
+ { name = "mlx" },
1413
+ { name = "numpy" },
1414
+ { name = "protobuf" },
1415
+ { name = "pyyaml" },
1416
+ { name = "transformers" },
1417
+ ]
1418
+ sdist = { url = "https://files.pythonhosted.org/packages/1c/d7/fdde445c7bd443a2ed23badda6064f1477c4051543922106f365e94082cd/mlx_lm-0.28.2.tar.gz", hash = "sha256:d28752635ed5c89ff2b41361916c928e6b16f765c07b2908044e1dcaf921ed9b", size = 209374, upload-time = "2025-10-02T14:23:57.497Z" }
1419
+ wheels = [
1420
+ { url = "https://files.pythonhosted.org/packages/f2/1c/89e0f60d45e364de8507065f73aeb8d2fd810d6cb95a9a512880b09399d5/mlx_lm-0.28.2-py3-none-any.whl", hash = "sha256:1501529e625d0d648216f7bb543b8b449d5fd17bd598f635536dbc1fbde6d1d6", size = 284600, upload-time = "2025-10-02T14:23:56.395Z" },
1421
+ ]
1422
+
1423
+ [[package]]
1424
+ name = "mlx-metal"
1425
+ version = "0.29.2"
1426
+ source = { registry = "https://pypi.org/simple" }
1427
+ wheels = [
1428
+ { url = "https://files.pythonhosted.org/packages/31/a5/a045006546fed791f6e9a74ed4451dac871d3c35f9e54a3a25d820668a85/mlx_metal-0.29.2-py3-none-macosx_13_0_arm64.whl", hash = "sha256:cf8f83a521e620357185c57945142718d526b9312ee112e5a89eb5600480f4d6", size = 35056194, upload-time = "2025-09-26T22:23:47.201Z" },
1429
+ { url = "https://files.pythonhosted.org/packages/4c/8c/4bdd3a7d04ed477b32aec30d30236dfca9f9ac27706cb309511278ddd281/mlx_metal-0.29.2-py3-none-macosx_14_0_arm64.whl", hash = "sha256:fa944001970813b296e8aff5616f2fa9daeda6bc1d190c17fbe8a7ca838ecef0", size = 34791708, upload-time = "2025-09-26T22:23:30.599Z" },
1430
+ { url = "https://files.pythonhosted.org/packages/b1/11/12e158848fe4d3316c999ffb6c2d88f554bde98d69022b3385e25ece997e/mlx_metal-0.29.2-py3-none-macosx_15_0_arm64.whl", hash = "sha256:08d8b7fe305425a14b74ebf36cee176575bfd4cd8d34a2aaae8f05b9983d2d71", size = 34784506, upload-time = "2025-09-26T22:23:29.207Z" },
1431
+ ]
1432
+
1433
  [[package]]
1434
  name = "mpmath"
1435
  version = "1.3.0"
 
2848
  { name = "typing-extensions", marker = "sys_platform == 'darwin'" },
2849
  ]
2850
  wheels = [
2851
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:3d05017d19bc99741288e458888283a44b0ee881d53f05f72f8b1cfea8998122" },
2852
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:a47b7986bee3f61ad217d8a8ce24605809ab425baf349f97de758815edd2ef54" },
2853
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:fbe2e149c5174ef90d29a5f84a554dfaf28e003cb4f61fa2c8c024c17ec7ca58" },
2854
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390" },
2855
  ]
2856
 
2857
  [[package]]
 
2873
  { name = "typing-extensions", marker = "sys_platform != 'darwin'" },
2874
  ]
2875
  wheels = [
2876
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-linux_s390x.whl", hash = "sha256:2bfc013dd6efdc8f8223a0241d3529af9f315dffefb53ffa3bf14d3f10127da6" },
2877
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:680129efdeeec3db5da3f88ee5d28c1b1e103b774aef40f9d638e2cce8f8d8d8" },
2878
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cb06175284673a581dd91fb1965662ae4ecaba6e5c357aa0ea7bb8b84b6b7eeb" },
2879
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:7631ef49fbd38d382909525b83696dc12a55d68492ade4ace3883c62b9fc140f" },
2880
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-win_arm64.whl", hash = "sha256:41e6fc5ec0914fcdce44ccf338b1d19a441b55cafdd741fd0bf1af3f9e4cfd14" },
2881
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:0e34e276722ab7dd0dffa9e12fe2135a9b34a0e300c456ed7ad6430229404eb5" },
2882
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:610f600c102386e581327d5efc18c0d6edecb9820b4140d26163354a99cd800d" },
2883
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cb9a8ba8137ab24e36bf1742cb79a1294bd374db570f09fc15a5e1318160db4e" },
2884
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:2be20b2c05a0cce10430cc25f32b689259640d273232b2de357c35729132256d" },
2885
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:99fc421a5d234580e45957a7b02effbf3e1c884a5dd077afc85352c77bf41434" },
2886
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:8b5882276633cf91fe3d2d7246c743b94d44a7e660b27f1308007fdb1bb89f7d" },
2887
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a5064b5e23772c8d164068cc7c12e01a75faf7b948ecd95a0d4007d7487e5f25" },
2888
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f81dedb4c6076ec325acc3b47525f9c550e5284a18eae1d9061c543f7b6e7de" },
2889
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:e1ee1b2346ade3ea90306dfbec7e8ff17bc220d344109d189ae09078333b0856" },
2890
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:64c187345509f2b1bb334feed4666e2c781ca381874bde589182f81247e61f88" },
2891
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af81283ac671f434b1b25c95ba295f270e72db1fad48831eb5e4748ff9840041" },
2892
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a9dbb6f64f63258bc811e2c0c99640a81e5af93c531ad96e95c5ec777ea46dab" },
2893
+ { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:6d93a7165419bc4b2b907e859ccab0dea5deeab261448ae9a5ec5431f14c0e64" },
2894
  ]
2895
 
2896
  [[package]]