lemonade-sdk 8.1.11__py3-none-any.whl → 8.2.2__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 lemonade-sdk might be problematic. Click here for more details.
- lemonade/cache.py +6 -1
- lemonade/common/status.py +4 -4
- lemonade/common/system_info.py +0 -26
- lemonade/tools/accuracy.py +143 -48
- lemonade/tools/adapter.py +6 -1
- lemonade/tools/bench.py +26 -8
- lemonade/tools/flm/utils.py +70 -22
- lemonade/tools/huggingface/bench.py +6 -1
- lemonade/tools/llamacpp/bench.py +146 -27
- lemonade/tools/llamacpp/load.py +30 -2
- lemonade/tools/llamacpp/utils.py +317 -21
- lemonade/tools/oga/bench.py +5 -26
- lemonade/tools/oga/load.py +49 -123
- lemonade/tools/oga/migration.py +403 -0
- lemonade/tools/report/table.py +76 -8
- lemonade/tools/server/flm.py +2 -6
- lemonade/tools/server/llamacpp.py +43 -2
- lemonade/tools/server/serve.py +354 -18
- lemonade/tools/server/static/js/chat.js +15 -77
- lemonade/tools/server/static/js/model-settings.js +24 -3
- lemonade/tools/server/static/js/models.js +440 -37
- lemonade/tools/server/static/js/shared.js +61 -8
- lemonade/tools/server/static/logs.html +157 -13
- lemonade/tools/server/static/styles.css +204 -0
- lemonade/tools/server/static/webapp.html +39 -1
- lemonade/version.py +1 -1
- lemonade_install/install.py +33 -579
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/METADATA +6 -4
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/RECORD +38 -37
- lemonade_server/cli.py +10 -0
- lemonade_server/model_manager.py +172 -11
- lemonade_server/pydantic_models.py +3 -0
- lemonade_server/server_models.json +102 -66
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/WHEEL +0 -0
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/entry_points.txt +0 -0
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/licenses/LICENSE +0 -0
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/licenses/NOTICE.md +0 -0
- {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/top_level.txt +0 -0
lemonade/tools/oga/load.py
CHANGED
|
@@ -38,6 +38,17 @@ execution_providers = {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
def find_onnx_files_recursively(directory):
|
|
42
|
+
"""
|
|
43
|
+
Recursively search for ONNX files in a directory and its subdirectories.
|
|
44
|
+
"""
|
|
45
|
+
for _, _, files in os.walk(directory):
|
|
46
|
+
for file in files:
|
|
47
|
+
if file.endswith(".onnx"):
|
|
48
|
+
return True
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
41
52
|
def _get_npu_driver_version():
|
|
42
53
|
"""
|
|
43
54
|
Get the NPU driver version using PowerShell directly.
|
|
@@ -321,6 +332,7 @@ class OgaLoad(FirstTool):
|
|
|
321
332
|
|
|
322
333
|
@staticmethod
|
|
323
334
|
def _setup_model_dependencies(full_model_path, device, ryzenai_version, oga_path):
|
|
335
|
+
# pylint: disable=unused-argument
|
|
324
336
|
"""
|
|
325
337
|
Sets up model dependencies for hybrid and NPU inference by:
|
|
326
338
|
1. Configuring the custom_ops_library path in genai_config.json.
|
|
@@ -328,76 +340,45 @@ class OgaLoad(FirstTool):
|
|
|
328
340
|
3. Check NPU driver version if required for device and ryzenai_version.
|
|
329
341
|
"""
|
|
330
342
|
|
|
331
|
-
|
|
343
|
+
# For RyzenAI 1.6.0, check NPU driver version for NPU and hybrid devices
|
|
344
|
+
if device in ["npu", "hybrid"]:
|
|
345
|
+
required_driver_version = REQUIRED_NPU_DRIVER_VERSION
|
|
332
346
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
required_driver_version = REQUIRED_NPU_DRIVER_VERSION
|
|
344
|
-
|
|
345
|
-
current_driver_version = _get_npu_driver_version()
|
|
346
|
-
|
|
347
|
-
if not current_driver_version:
|
|
348
|
-
printing.log_warning(
|
|
349
|
-
f"NPU driver not found. {device.upper()} inference requires NPU driver "
|
|
350
|
-
f"version {required_driver_version}.\n"
|
|
351
|
-
"Please download and install the NPU Driver from:\n"
|
|
352
|
-
f"{NPU_DRIVER_DOWNLOAD_URL}\n"
|
|
353
|
-
"NPU functionality may not work properly."
|
|
354
|
-
)
|
|
355
|
-
_open_driver_install_page()
|
|
356
|
-
|
|
357
|
-
elif not _compare_driver_versions(
|
|
358
|
-
current_driver_version, required_driver_version
|
|
359
|
-
):
|
|
360
|
-
printing.log_warning(
|
|
361
|
-
f"Incorrect NPU driver version detected: {current_driver_version}\n"
|
|
362
|
-
f"{device.upper()} inference with RyzenAI 1.5.0 requires driver "
|
|
363
|
-
f"version {required_driver_version} or higher.\n"
|
|
364
|
-
"Please download and install the correct NPU Driver from:\n"
|
|
365
|
-
f"{NPU_DRIVER_DOWNLOAD_URL}\n"
|
|
366
|
-
"NPU functionality may not work properly."
|
|
367
|
-
)
|
|
368
|
-
_open_driver_install_page()
|
|
369
|
-
|
|
370
|
-
if device == "npu":
|
|
371
|
-
# For 1.5.0, custom ops are in the conda environment's onnxruntime package
|
|
372
|
-
custom_ops_path = os.path.join(
|
|
373
|
-
env_path,
|
|
374
|
-
"Lib",
|
|
375
|
-
"site-packages",
|
|
376
|
-
"onnxruntime",
|
|
377
|
-
"capi",
|
|
378
|
-
"onnxruntime_vitis_ai_custom_ops.dll",
|
|
379
|
-
)
|
|
380
|
-
dll_source_path = os.path.join(
|
|
381
|
-
env_path, "Lib", "site-packages", "onnxruntime", "capi"
|
|
382
|
-
)
|
|
383
|
-
required_dlls = ["dyn_dispatch_core.dll", "xaiengine.dll"]
|
|
384
|
-
else:
|
|
385
|
-
custom_ops_path = os.path.join(
|
|
386
|
-
env_path,
|
|
387
|
-
"Lib",
|
|
388
|
-
"site-packages",
|
|
389
|
-
"onnxruntime_genai",
|
|
390
|
-
"onnx_custom_ops.dll",
|
|
347
|
+
current_driver_version = _get_npu_driver_version()
|
|
348
|
+
rai_version, _ = _get_ryzenai_version_info(device)
|
|
349
|
+
|
|
350
|
+
if not current_driver_version:
|
|
351
|
+
printing.log_warning(
|
|
352
|
+
f"NPU driver not found. {device.upper()} inference requires NPU driver "
|
|
353
|
+
f"version {required_driver_version}.\n"
|
|
354
|
+
"Please download and install the NPU Driver from:\n"
|
|
355
|
+
f"{NPU_DRIVER_DOWNLOAD_URL}\n"
|
|
356
|
+
"NPU functionality may not work properly."
|
|
391
357
|
)
|
|
392
|
-
|
|
393
|
-
|
|
358
|
+
_open_driver_install_page()
|
|
359
|
+
|
|
360
|
+
elif not _compare_driver_versions(
|
|
361
|
+
current_driver_version, required_driver_version
|
|
362
|
+
):
|
|
363
|
+
printing.log_warning(
|
|
364
|
+
f"Incorrect NPU driver version detected: {current_driver_version}\n"
|
|
365
|
+
f"{device.upper()} inference with RyzenAI {rai_version} requires driver "
|
|
366
|
+
f"version {required_driver_version} or higher.\n"
|
|
367
|
+
"Please download and install the correct NPU Driver from:\n"
|
|
368
|
+
f"{NPU_DRIVER_DOWNLOAD_URL}\n"
|
|
369
|
+
"NPU functionality may not work properly."
|
|
394
370
|
)
|
|
395
|
-
|
|
371
|
+
_open_driver_install_page()
|
|
372
|
+
|
|
373
|
+
# Setup DLL paths for NPU/hybrid inference
|
|
374
|
+
env_path = os.path.dirname(sys.executable)
|
|
375
|
+
dll_source_path = os.path.join(
|
|
376
|
+
env_path, "Lib", "site-packages", "onnxruntime_genai"
|
|
377
|
+
)
|
|
378
|
+
required_dlls = ["libutf8_validity.dll", "abseil_dll.dll"]
|
|
396
379
|
|
|
397
380
|
# Validate that all required DLLs exist in the source directory
|
|
398
381
|
missing_dlls = []
|
|
399
|
-
if not os.path.exists(custom_ops_path):
|
|
400
|
-
missing_dlls.append(custom_ops_path)
|
|
401
382
|
|
|
402
383
|
for dll_name in required_dlls:
|
|
403
384
|
dll_source = os.path.join(dll_source_path, dll_name)
|
|
@@ -408,7 +389,9 @@ class OgaLoad(FirstTool):
|
|
|
408
389
|
dll_list = "\n - ".join(missing_dlls)
|
|
409
390
|
raise RuntimeError(
|
|
410
391
|
f"Required DLLs not found for {device} inference:\n - {dll_list}\n"
|
|
411
|
-
f"Please ensure your RyzenAI installation is complete and supports {device}
|
|
392
|
+
f"Please ensure your RyzenAI installation is complete and supports {device}.\n"
|
|
393
|
+
"Please reinstall the RyzenAI Software for your platform. Run:\n"
|
|
394
|
+
" pip install lemonade-sdk[oga-ryzenai]\n"
|
|
412
395
|
)
|
|
413
396
|
|
|
414
397
|
# Add the DLL source directory to PATH
|
|
@@ -416,29 +399,6 @@ class OgaLoad(FirstTool):
|
|
|
416
399
|
if dll_source_path not in current_path:
|
|
417
400
|
os.environ["PATH"] = dll_source_path + os.pathsep + current_path
|
|
418
401
|
|
|
419
|
-
# Update the model config with custom_ops_library path
|
|
420
|
-
config_path = os.path.join(full_model_path, "genai_config.json")
|
|
421
|
-
if os.path.exists(config_path):
|
|
422
|
-
with open(config_path, "r", encoding="utf-8") as f:
|
|
423
|
-
config = json.load(f)
|
|
424
|
-
|
|
425
|
-
if (
|
|
426
|
-
"model" in config
|
|
427
|
-
and "decoder" in config["model"]
|
|
428
|
-
and "session_options" in config["model"]["decoder"]
|
|
429
|
-
):
|
|
430
|
-
config["model"]["decoder"]["session_options"][
|
|
431
|
-
"custom_ops_library"
|
|
432
|
-
] = custom_ops_path
|
|
433
|
-
|
|
434
|
-
with open(config_path, "w", encoding="utf-8") as f:
|
|
435
|
-
json.dump(config, f, indent=4)
|
|
436
|
-
|
|
437
|
-
else:
|
|
438
|
-
printing.log_info(
|
|
439
|
-
f"Model's `genai_config.json` not found in {full_model_path}"
|
|
440
|
-
)
|
|
441
|
-
|
|
442
402
|
@staticmethod
|
|
443
403
|
def _is_preoptimized_model(input_model_path):
|
|
444
404
|
"""
|
|
@@ -502,34 +462,6 @@ class OgaLoad(FirstTool):
|
|
|
502
462
|
|
|
503
463
|
return full_model_path
|
|
504
464
|
|
|
505
|
-
@staticmethod
|
|
506
|
-
def _setup_npu_environment(ryzenai_version, oga_path):
|
|
507
|
-
"""
|
|
508
|
-
Sets up environment for NPU flow of ONNX model and returns saved state to be restored
|
|
509
|
-
later in cleanup.
|
|
510
|
-
"""
|
|
511
|
-
if "1.5.0" in ryzenai_version:
|
|
512
|
-
# For PyPI installation (1.5.0+), no environment setup needed
|
|
513
|
-
return None
|
|
514
|
-
elif "1.4.0" in ryzenai_version:
|
|
515
|
-
# Legacy lemonade-install approach for 1.4.0
|
|
516
|
-
if not os.path.exists(os.path.join(oga_path, "libs", "onnxruntime.dll")):
|
|
517
|
-
raise RuntimeError(
|
|
518
|
-
f"Cannot find libs/onnxruntime.dll in lib folder: {oga_path}"
|
|
519
|
-
)
|
|
520
|
-
|
|
521
|
-
# Save current state so they can be restored after inference.
|
|
522
|
-
saved_state = {"cwd": os.getcwd(), "path": os.environ["PATH"]}
|
|
523
|
-
|
|
524
|
-
# Setup NPU environment (cwd and path will be restored later)
|
|
525
|
-
os.chdir(oga_path)
|
|
526
|
-
os.environ["PATH"] = (
|
|
527
|
-
os.path.join(oga_path, "libs") + os.pathsep + os.environ["PATH"]
|
|
528
|
-
)
|
|
529
|
-
return saved_state
|
|
530
|
-
else:
|
|
531
|
-
raise ValueError(f"Unsupported RyzenAI version: {ryzenai_version}")
|
|
532
|
-
|
|
533
465
|
@staticmethod
|
|
534
466
|
def _load_model_and_setup_state(
|
|
535
467
|
state, full_model_path, checkpoint, trust_remote_code
|
|
@@ -702,8 +634,7 @@ class OgaLoad(FirstTool):
|
|
|
702
634
|
state.save_stat(Keys.CHECKPOINT, checkpoint)
|
|
703
635
|
state.save_stat(Keys.LOCAL_MODEL_FOLDER, full_model_path)
|
|
704
636
|
# See if there is a file ending in ".onnx" in this folder
|
|
705
|
-
|
|
706
|
-
has_onnx_file = any([filename.endswith(".onnx") for filename in dir])
|
|
637
|
+
has_onnx_file = find_onnx_files_recursively(input)
|
|
707
638
|
if not has_onnx_file:
|
|
708
639
|
raise ValueError(
|
|
709
640
|
f"The folder {input} does not contain an ONNX model file."
|
|
@@ -852,15 +783,10 @@ class OgaLoad(FirstTool):
|
|
|
852
783
|
|
|
853
784
|
try:
|
|
854
785
|
if device == "npu":
|
|
855
|
-
saved_env_state = self._setup_npu_environment(
|
|
856
|
-
ryzenai_version, oga_path
|
|
857
|
-
)
|
|
858
786
|
# Set USE_AIE_RoPE based on model type
|
|
859
787
|
os.environ["USE_AIE_RoPE"] = (
|
|
860
788
|
"0" if "phi-" in checkpoint.lower() else "1"
|
|
861
789
|
)
|
|
862
|
-
elif device == "hybrid":
|
|
863
|
-
saved_env_state = None
|
|
864
790
|
|
|
865
791
|
self._load_model_and_setup_state(
|
|
866
792
|
state, full_model_path, checkpoint, trust_remote_code
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Migration utilities for handling RyzenAI version upgrades.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to detect and clean up incompatible RyzenAI models
|
|
5
|
+
when upgrading between major versions (e.g., 1.4/1.5 -> 1.6).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import json
|
|
10
|
+
import shutil
|
|
11
|
+
import logging
|
|
12
|
+
from typing import List, Dict, Optional, Tuple
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_directory_size(path: str) -> int:
|
|
16
|
+
"""
|
|
17
|
+
Calculate the total size of a directory in bytes.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
path: Path to the directory
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Total size in bytes
|
|
24
|
+
"""
|
|
25
|
+
total_size = 0
|
|
26
|
+
try:
|
|
27
|
+
for dirpath, _, filenames in os.walk(path):
|
|
28
|
+
for filename in filenames:
|
|
29
|
+
filepath = os.path.join(dirpath, filename)
|
|
30
|
+
try:
|
|
31
|
+
total_size += os.path.getsize(filepath)
|
|
32
|
+
except (OSError, FileNotFoundError):
|
|
33
|
+
# Skip files that can't be accessed
|
|
34
|
+
pass
|
|
35
|
+
except (OSError, FileNotFoundError):
|
|
36
|
+
pass
|
|
37
|
+
return total_size
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def format_size(size_bytes: int) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Format byte size to human-readable string.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
size_bytes: Size in bytes
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Formatted string (e.g., "1.5 GB", "450 MB")
|
|
49
|
+
"""
|
|
50
|
+
for unit in ["B", "KB", "MB"]:
|
|
51
|
+
if size_bytes < 1024.0:
|
|
52
|
+
return f"{size_bytes:.1f} {unit}"
|
|
53
|
+
size_bytes /= 1024.0
|
|
54
|
+
return f"{size_bytes:.1f} GB"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def check_rai_config_version(model_path: str, required_version: str = "1.6.0") -> bool:
|
|
58
|
+
"""
|
|
59
|
+
Check if a model's rai_config.json contains the required version.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
model_path: Path to the model directory
|
|
63
|
+
required_version: Version string to check for (default: "1.6.0")
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
True if model is compatible (has required version), False otherwise
|
|
67
|
+
"""
|
|
68
|
+
rai_config_path = os.path.join(model_path, "rai_config.json")
|
|
69
|
+
|
|
70
|
+
# If no rai_config.json exists, it's not a RyzenAI model
|
|
71
|
+
if not os.path.exists(rai_config_path):
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
with open(rai_config_path, "r", encoding="utf-8") as f:
|
|
76
|
+
config = json.load(f)
|
|
77
|
+
|
|
78
|
+
# Check if max_prompt_length exists and has the required version
|
|
79
|
+
if "max_prompt_length" in config:
|
|
80
|
+
max_prompt_length = config["max_prompt_length"]
|
|
81
|
+
if isinstance(max_prompt_length, dict):
|
|
82
|
+
# If it's a dict with version keys, check for required version
|
|
83
|
+
return required_version in max_prompt_length
|
|
84
|
+
# Fallback to True to avoid deleting models if format changes
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
90
|
+
logging.warning(f"Could not read rai_config.json from {model_path}: {e}")
|
|
91
|
+
# If we can't read it, assume it's compatible to avoid false positives
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def scan_oga_models_cache(cache_dir: str) -> List[Dict[str, any]]:
|
|
96
|
+
"""
|
|
97
|
+
Scan the Lemonade OGA models cache for incompatible models.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
cache_dir: Path to the Lemonade cache directory
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
List of dicts with model info (path, name, size, compatible)
|
|
104
|
+
"""
|
|
105
|
+
oga_models_path = os.path.join(cache_dir, "oga_models")
|
|
106
|
+
incompatible_models = []
|
|
107
|
+
|
|
108
|
+
if not os.path.exists(oga_models_path):
|
|
109
|
+
return incompatible_models
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
# Iterate through model directories in oga_models
|
|
113
|
+
for model_name in os.listdir(oga_models_path):
|
|
114
|
+
model_dir = os.path.join(oga_models_path, model_name)
|
|
115
|
+
|
|
116
|
+
if not os.path.isdir(model_dir):
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
# Check all subdirectories (e.g., npu-int4, hybrid-int4)
|
|
120
|
+
for subdir in os.listdir(model_dir):
|
|
121
|
+
subdir_path = os.path.join(model_dir, subdir)
|
|
122
|
+
|
|
123
|
+
if not os.path.isdir(subdir_path):
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
# Check if this model version is compatible
|
|
127
|
+
if not check_rai_config_version(subdir_path):
|
|
128
|
+
size = get_directory_size(subdir_path)
|
|
129
|
+
incompatible_models.append(
|
|
130
|
+
{
|
|
131
|
+
"path": subdir_path,
|
|
132
|
+
"name": f"{model_name}/{subdir}",
|
|
133
|
+
"size": size,
|
|
134
|
+
"size_formatted": format_size(size),
|
|
135
|
+
"cache_type": "lemonade",
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
except (OSError, PermissionError) as e:
|
|
140
|
+
logging.warning(f"Error scanning oga_models cache: {e}")
|
|
141
|
+
|
|
142
|
+
return incompatible_models
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def scan_huggingface_cache(hf_home: Optional[str] = None) -> List[Dict[str, any]]:
|
|
146
|
+
"""
|
|
147
|
+
Scan the HuggingFace cache for incompatible RyzenAI models.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
hf_home: Path to HuggingFace home directory (default: from env or ~/.cache/huggingface)
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
List of dicts with model info (path, name, size, compatible)
|
|
154
|
+
"""
|
|
155
|
+
if hf_home is None:
|
|
156
|
+
hf_home = os.environ.get(
|
|
157
|
+
"HF_HOME", os.path.join(os.path.expanduser("~"), ".cache", "huggingface")
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
hub_path = os.path.join(hf_home, "hub")
|
|
161
|
+
incompatible_models = []
|
|
162
|
+
|
|
163
|
+
if not os.path.exists(hub_path):
|
|
164
|
+
return incompatible_models
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
# Iterate through model directories in HuggingFace cache
|
|
168
|
+
for item in os.listdir(hub_path):
|
|
169
|
+
if not item.startswith("models--"):
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
model_dir = os.path.join(hub_path, item)
|
|
173
|
+
if not os.path.isdir(model_dir):
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
# Look in snapshots subdirectory
|
|
177
|
+
snapshots_dir = os.path.join(model_dir, "snapshots")
|
|
178
|
+
if not os.path.exists(snapshots_dir):
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
# Check each snapshot
|
|
182
|
+
for snapshot_hash in os.listdir(snapshots_dir):
|
|
183
|
+
snapshot_path = os.path.join(snapshots_dir, snapshot_hash)
|
|
184
|
+
|
|
185
|
+
if not os.path.isdir(snapshot_path):
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
# Check if this snapshot has incompatible RyzenAI model
|
|
189
|
+
if not check_rai_config_version(snapshot_path):
|
|
190
|
+
# Extract readable model name from directory
|
|
191
|
+
model_name = item.replace("models--", "").replace("--", "/")
|
|
192
|
+
size = get_directory_size(
|
|
193
|
+
model_dir
|
|
194
|
+
) # Size of entire model directory
|
|
195
|
+
incompatible_models.append(
|
|
196
|
+
{
|
|
197
|
+
"path": model_dir,
|
|
198
|
+
"name": model_name,
|
|
199
|
+
"size": size,
|
|
200
|
+
"size_formatted": format_size(size),
|
|
201
|
+
"cache_type": "huggingface",
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
break
|
|
205
|
+
|
|
206
|
+
except (OSError, PermissionError) as e:
|
|
207
|
+
logging.warning(f"Error scanning HuggingFace cache: {e}")
|
|
208
|
+
|
|
209
|
+
return incompatible_models
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def detect_incompatible_ryzenai_models(
|
|
213
|
+
cache_dir: str, hf_home: Optional[str] = None
|
|
214
|
+
) -> Tuple[List[Dict[str, any]], int]:
|
|
215
|
+
"""
|
|
216
|
+
Detect all incompatible RyzenAI models in both Lemonade and HuggingFace caches.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
cache_dir: Path to the Lemonade cache directory
|
|
220
|
+
hf_home: Path to HuggingFace home directory (optional)
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Tuple of (list of incompatible models, total size in bytes)
|
|
224
|
+
"""
|
|
225
|
+
incompatible_models = []
|
|
226
|
+
|
|
227
|
+
# Scan Lemonade cache
|
|
228
|
+
oga_models = scan_oga_models_cache(cache_dir)
|
|
229
|
+
incompatible_models.extend(oga_models)
|
|
230
|
+
|
|
231
|
+
# Scan HuggingFace cache
|
|
232
|
+
hf_models = scan_huggingface_cache(hf_home)
|
|
233
|
+
incompatible_models.extend(hf_models)
|
|
234
|
+
|
|
235
|
+
# Calculate total size
|
|
236
|
+
total_size = sum(model["size"] for model in incompatible_models)
|
|
237
|
+
|
|
238
|
+
logging.info(
|
|
239
|
+
f"Found {len(incompatible_models)} incompatible RyzenAI models "
|
|
240
|
+
f"({format_size(total_size)} total)"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return incompatible_models, total_size
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def delete_model_directory(model_path: str) -> bool:
|
|
247
|
+
"""
|
|
248
|
+
Safely delete a model directory.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
model_path: Path to the model directory to delete
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
True if deletion successful, False otherwise
|
|
255
|
+
"""
|
|
256
|
+
try:
|
|
257
|
+
if os.path.exists(model_path):
|
|
258
|
+
shutil.rmtree(model_path)
|
|
259
|
+
logging.info(f"Deleted model directory: {model_path}")
|
|
260
|
+
return True
|
|
261
|
+
else:
|
|
262
|
+
logging.warning(f"Model directory not found: {model_path}")
|
|
263
|
+
return False
|
|
264
|
+
except (OSError, PermissionError) as e:
|
|
265
|
+
logging.error(f"Failed to delete model directory {model_path}: {e}")
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _extract_checkpoint_from_path(path: str) -> Optional[str]:
|
|
270
|
+
"""
|
|
271
|
+
Extract the checkpoint name from a model path.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
path: Model directory path (either Lemonade cache or HuggingFace cache)
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Checkpoint name (e.g., "amd/Qwen2.5-1.5B-Instruct-awq") or None if not extractable
|
|
278
|
+
"""
|
|
279
|
+
# Normalize path separators to handle both Unix and Windows paths
|
|
280
|
+
normalized_path = path.replace("\\", "/")
|
|
281
|
+
parts = normalized_path.split("/")
|
|
282
|
+
|
|
283
|
+
# Handle HuggingFace cache paths: models--{org}--{repo}
|
|
284
|
+
if "models--" in normalized_path:
|
|
285
|
+
for part in parts:
|
|
286
|
+
if part.startswith("models--"):
|
|
287
|
+
# Convert models--org--repo to org/repo
|
|
288
|
+
# Replace first two occurrences of -- with /
|
|
289
|
+
checkpoint = part.replace("models--", "", 1).replace("--", "/", 1)
|
|
290
|
+
return checkpoint
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
# Handle Lemonade cache paths: oga_models/{model_name}/{device}-{dtype}
|
|
294
|
+
if "oga_models" in normalized_path:
|
|
295
|
+
try:
|
|
296
|
+
oga_models_idx = parts.index("oga_models")
|
|
297
|
+
if oga_models_idx + 1 < len(parts):
|
|
298
|
+
model_name = parts[oga_models_idx + 1]
|
|
299
|
+
# Convert model_name back to checkpoint (e.g., amd_model -> amd/model)
|
|
300
|
+
# This is a heuristic - we look for the pattern {org}_{model}
|
|
301
|
+
checkpoint = model_name.replace("_", "/", 1)
|
|
302
|
+
return checkpoint
|
|
303
|
+
except (ValueError, IndexError):
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _cleanup_user_models_json(deleted_checkpoints: List[str], user_models_file: str):
|
|
310
|
+
"""
|
|
311
|
+
Remove entries from user_models.json for models that have been deleted.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
deleted_checkpoints: List of checkpoint names that were deleted
|
|
315
|
+
user_models_file: Path to user_models.json
|
|
316
|
+
"""
|
|
317
|
+
if not deleted_checkpoints or not os.path.exists(user_models_file):
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
with open(user_models_file, "r", encoding="utf-8") as f:
|
|
322
|
+
user_models = json.load(f)
|
|
323
|
+
|
|
324
|
+
# Track which models to remove
|
|
325
|
+
models_to_remove = []
|
|
326
|
+
for model_name, model_info in user_models.items():
|
|
327
|
+
checkpoint = model_info.get("checkpoint", "")
|
|
328
|
+
# Check if this checkpoint matches any deleted checkpoints
|
|
329
|
+
# We do a case-insensitive comparison since paths may have been lowercased
|
|
330
|
+
for deleted_checkpoint in deleted_checkpoints:
|
|
331
|
+
if checkpoint.lower() == deleted_checkpoint.lower():
|
|
332
|
+
models_to_remove.append(model_name)
|
|
333
|
+
break
|
|
334
|
+
|
|
335
|
+
# Remove the models
|
|
336
|
+
for model_name in models_to_remove:
|
|
337
|
+
del user_models[model_name]
|
|
338
|
+
logging.info(f"Removed {model_name} from user_models.json")
|
|
339
|
+
|
|
340
|
+
# Save the updated file only if we removed something
|
|
341
|
+
if models_to_remove:
|
|
342
|
+
with open(user_models_file, "w", encoding="utf-8") as f:
|
|
343
|
+
json.dump(user_models, f, indent=2)
|
|
344
|
+
logging.info(
|
|
345
|
+
f"Updated user_models.json - removed {len(models_to_remove)} entries"
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
349
|
+
logging.warning(f"Could not update user_models.json: {e}")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def delete_incompatible_models(
|
|
353
|
+
model_paths: List[str], user_models_file: Optional[str] = None
|
|
354
|
+
) -> Dict[str, any]:
|
|
355
|
+
"""
|
|
356
|
+
Delete multiple incompatible model directories and clean up user_models.json.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
model_paths: List of paths to delete
|
|
360
|
+
user_models_file: Path to user_models.json (optional, will use default if not provided)
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
Dict with deletion results (success_count, failed_count, freed_size, cleaned_user_models)
|
|
364
|
+
"""
|
|
365
|
+
success_count = 0
|
|
366
|
+
failed_count = 0
|
|
367
|
+
freed_size = 0
|
|
368
|
+
deleted_checkpoints = []
|
|
369
|
+
|
|
370
|
+
for path in model_paths:
|
|
371
|
+
# Calculate size before deletion
|
|
372
|
+
size = get_directory_size(path)
|
|
373
|
+
|
|
374
|
+
# Extract checkpoint name before deleting
|
|
375
|
+
checkpoint = _extract_checkpoint_from_path(path)
|
|
376
|
+
if checkpoint:
|
|
377
|
+
deleted_checkpoints.append(checkpoint)
|
|
378
|
+
|
|
379
|
+
if delete_model_directory(path):
|
|
380
|
+
success_count += 1
|
|
381
|
+
freed_size += size
|
|
382
|
+
else:
|
|
383
|
+
failed_count += 1
|
|
384
|
+
|
|
385
|
+
# Clean up user_models.json if we deleted any models
|
|
386
|
+
cleaned_user_models = False
|
|
387
|
+
if deleted_checkpoints:
|
|
388
|
+
# Use default path if not provided
|
|
389
|
+
if user_models_file is None:
|
|
390
|
+
from lemonade.cache import DEFAULT_CACHE_DIR
|
|
391
|
+
|
|
392
|
+
user_models_file = os.path.join(DEFAULT_CACHE_DIR, "user_models.json")
|
|
393
|
+
|
|
394
|
+
_cleanup_user_models_json(deleted_checkpoints, user_models_file)
|
|
395
|
+
cleaned_user_models = True
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
"success_count": success_count,
|
|
399
|
+
"failed_count": failed_count,
|
|
400
|
+
"freed_size": freed_size,
|
|
401
|
+
"freed_size_formatted": format_size(freed_size),
|
|
402
|
+
"cleaned_user_models": cleaned_user_models,
|
|
403
|
+
}
|