Skip to content

Commit fdc2538

Browse files
fix: block MetalRT on M1/M2 with clear error (requires Apple M3+ for Metal 3.1)
MetalRT shaders use bfloat16 and -std=metal3.1, which requires Apple GPU Family 9 (M3 or later). M1/M2 chips cannot load the metallib at all. Added gpu_supported() check to every MetalRT entry point: - metalrt_loader load()/install(): rejects early with clear message - rcli setup: auto-selects llama.cpp on M1/M2, hides MetalRT choice - rcli engine metalrt: prints "requires M3+" error - rcli metalrt install: same check - TUI engine switcher: shows "Requires Apple M3 or later" label - rcli_init: graceful fallback to llama.cpp with warning Made-with: Cursor
1 parent ea46900 commit fdc2538

7 files changed

Lines changed: 102 additions & 20 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ if(POLICY CMP0177)
55
endif()
66
set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "" FORCE)
77

8-
project(rcli VERSION 0.2.4 LANGUAGES C CXX)
8+
project(rcli VERSION 0.2.5 LANGUAGES C CXX)
99

1010
set(CMAKE_CXX_STANDARD 17)
1111
set(CMAKE_CXX_STANDARD_REQUIRED ON)

src/api/rcli_api.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,11 @@ int rcli_init(RCLIHandle handle, const char* models_dir, int gpu_layers) {
334334
// --- MetalRT (optional, based on user engine preference) ---
335335
{
336336
std::string engine_pref = rcli::read_engine_preference();
337+
if (engine_pref == "metalrt" && !rastack::MetalRTLoader::gpu_supported()) {
338+
LOG_WARN("RCLI", "MetalRT requires Apple M3+ (Metal 3.1). Falling back to llama.cpp.");
339+
fprintf(stderr, " MetalRT requires Apple M3 or later. Falling back to llama.cpp.\n");
340+
engine_pref = "llamacpp";
341+
}
337342
if (engine_pref == "metalrt") {
338343
auto& mrt_loader = rastack::MetalRTLoader::instance();
339344
if (mrt_loader.is_available()) {

src/cli/main.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,13 @@ static int cmd_rag(const Args& args) {
590590

591591
static int cmd_metalrt(const Args& args) {
592592
if (args.arg1 == "install") {
593+
if (!rastack::MetalRTLoader::gpu_supported()) {
594+
fprintf(stderr, "\n %s%sMetalRT requires Apple M3 or later.%s\n"
595+
" Your Mac uses an M1/M2 chip which doesn't support Metal 3.1 shaders.\n"
596+
" Please use llama.cpp instead: %srcli engine llamacpp%s\n\n",
597+
color::bold, color::red, color::reset, color::bold, color::reset);
598+
return 1;
599+
}
593600
auto& loader = rastack::MetalRTLoader::instance();
594601
if (loader.is_available()) {
595602
std::string ver = rastack::MetalRTLoader::installed_version();
@@ -910,6 +917,13 @@ static int cmd_engine(const Args& args) {
910917
std::string target = args.arg1;
911918

912919
if (target == "metalrt") {
920+
if (!rastack::MetalRTLoader::gpu_supported()) {
921+
fprintf(stderr, "\n %s%sMetalRT requires Apple M3 or later.%s\n"
922+
" Your Mac uses an M1/M2 chip which doesn't support Metal 3.1 shaders.\n"
923+
" Please use llama.cpp instead: %srcli engine llamacpp%s\n\n",
924+
color::bold, color::red, color::reset, color::bold, color::reset);
925+
return 1;
926+
}
913927
if (!rastack::MetalRTLoader::instance().is_available()) {
914928
fprintf(stderr, "\n MetalRT not found. Installing automatically...\n\n");
915929
if (!rastack::MetalRTLoader::install()) {

src/cli/setup_cmds.h

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,29 +55,44 @@ inline int cmd_setup(const Args& args) {
5555
}
5656

5757
// --- Engine choice ---
58+
bool metalrt_gpu_ok = rastack::MetalRTLoader::gpu_supported();
59+
5860
fprintf(stderr, " Choose your inference engine:\n\n");
5961
fprintf(stderr, " %s1%s %sOpen Source%s (llama.cpp + sherpa-onnx) ~1 GB\n",
6062
color::bold, color::reset, color::green, color::reset);
6163
fprintf(stderr, " Community-maintained, all models supported.\n");
6264
fprintf(stderr, " Downloads: LFM2 1.2B + Whisper + Piper TTS\n\n");
63-
fprintf(stderr, " %s2%s %sMetalRT%s (Apple Silicon GPU acceleration) ~0.9 GB\n",
64-
color::bold, color::reset, color::cyan, color::reset);
65-
fprintf(stderr, " GPU-accelerated engine: ~550 tok/s (LFM2.5 1.2B)\n");
66-
fprintf(stderr, " Downloads: LFM2.5 1.2B + Whisper Tiny + Kokoro TTS\n");
67-
fprintf(stderr, " More models available on-demand via %srcli models%s\n\n",
68-
color::bold, color::reset);
69-
fprintf(stderr, " %s3%s %sBoth%s (recommended) ~1.9 GB\n",
70-
color::bold, color::reset, color::orange, color::reset);
71-
fprintf(stderr, " Install both engines. Use MetalRT when available,\n");
72-
fprintf(stderr, " fall back to llama.cpp for unsupported models.\n\n");
73-
fprintf(stderr, " Enter choice [1-3]: ");
65+
if (metalrt_gpu_ok) {
66+
fprintf(stderr, " %s2%s %sMetalRT%s (Apple Silicon GPU acceleration) ~0.9 GB\n",
67+
color::bold, color::reset, color::cyan, color::reset);
68+
fprintf(stderr, " GPU-accelerated engine: ~550 tok/s (LFM2.5 1.2B)\n");
69+
fprintf(stderr, " Downloads: LFM2.5 1.2B + Whisper Tiny + Kokoro TTS\n");
70+
fprintf(stderr, " More models available on-demand via %srcli models%s\n\n",
71+
color::bold, color::reset);
72+
fprintf(stderr, " %s3%s %sBoth%s (recommended) ~1.9 GB\n",
73+
color::bold, color::reset, color::orange, color::reset);
74+
fprintf(stderr, " Install both engines. Use MetalRT when available,\n");
75+
fprintf(stderr, " fall back to llama.cpp for unsupported models.\n\n");
76+
fprintf(stderr, " Enter choice [1-3]: ");
77+
} else {
78+
fprintf(stderr, " %s2%s %sMetalRT%s %s(requires Apple M3 or later)%s\n\n",
79+
color::bold, color::reset, color::cyan, color::reset,
80+
color::dim, color::reset);
81+
fprintf(stderr, " Your Mac doesn't support MetalRT (Metal 3.1 required).\n");
82+
fprintf(stderr, " Installing llama.cpp automatically.\n\n");
83+
}
7484
fflush(stderr);
7585

76-
char engine_buf[16] = {};
77-
if (read(STDIN_FILENO, engine_buf, sizeof(engine_buf) - 1) <= 0) engine_buf[0] = '3';
78-
if (engine_buf[0] == '\n') engine_buf[0] = '3';
79-
int engine_choice = engine_buf[0] - '0';
80-
if (engine_choice < 1 || engine_choice > 3) engine_choice = 3;
86+
int engine_choice;
87+
if (!metalrt_gpu_ok) {
88+
engine_choice = 1;
89+
} else {
90+
char engine_buf[16] = {};
91+
if (read(STDIN_FILENO, engine_buf, sizeof(engine_buf) - 1) <= 0) engine_buf[0] = '3';
92+
if (engine_buf[0] == '\n') engine_buf[0] = '3';
93+
engine_choice = engine_buf[0] - '0';
94+
if (engine_choice < 1 || engine_choice > 3) engine_choice = 3;
95+
}
8196

8297
bool install_llamacpp = (engine_choice == 1 || engine_choice == 3);
8398
bool install_metalrt = (engine_choice == 2 || engine_choice == 3);

src/cli/tui_app.h

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,8 +1363,9 @@ class TuiApp {
13631363
std::string current = rcli::read_engine_preference();
13641364
if (current.empty()) current = "auto";
13651365

1366+
bool metalrt_gpu_ok = rastack::MetalRTLoader::gpu_supported();
13661367
bool metalrt_available = false;
1367-
{
1368+
if (metalrt_gpu_ok) {
13681369
std::string dylib_path = rastack::MetalRTLoader::engines_dir() + "/libmetalrt.dylib";
13691370
struct stat st;
13701371
metalrt_available = (stat(dylib_path.c_str(), &st) == 0);
@@ -1375,10 +1376,13 @@ class TuiApp {
13751376
"CPU inference \u00B7 GGUF models \u00B7 Universal compatibility",
13761377
current == "llamacpp"});
13771378

1379+
std::string mrt_desc = metalrt_gpu_ok
1380+
? (std::string("GPU-accelerated \u00B7 MLX 4-bit \u00B7 Apple Silicon optimized") +
1381+
(metalrt_available ? "" : " [not installed]"))
1382+
: "Requires Apple M3 or later";
13781383
engine_entries_.push_back({"metalrt",
13791384
"MetalRT",
1380-
std::string("GPU-accelerated \u00B7 MLX 4-bit \u00B7 Apple Silicon optimized") +
1381-
(metalrt_available ? "" : " [not installed]"),
1385+
mrt_desc,
13821386
current == "metalrt"});
13831387

13841388
if (current == "auto") {
@@ -1401,6 +1405,11 @@ class TuiApp {
14011405
}
14021406

14031407
if (sel.id == "metalrt") {
1408+
if (!rastack::MetalRTLoader::gpu_supported()) {
1409+
engine_message_ = "MetalRT requires Apple M3 or later. Use llama.cpp instead.";
1410+
engine_msg_color_ = ftxui::Color::Red;
1411+
return;
1412+
}
14041413
std::string dylib_path = rastack::MetalRTLoader::engines_dir() + "/libmetalrt.dylib";
14051414
struct stat st;
14061415
if (stat(dylib_path.c_str(), &st) != 0) {

src/engines/metalrt_loader.cpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@
55
#include <cstdio>
66
#include <cstdlib>
77
#include <mach-o/dyld.h>
8+
#include <sys/sysctl.h>
9+
10+
static bool gpu_supports_metal31() {
11+
char chip[64] = {};
12+
size_t len = sizeof(chip);
13+
if (sysctlbyname("machdep.cpu.brand_string", chip, &len, nullptr, 0) != 0)
14+
return false;
15+
// M3, M3 Pro, M3 Max, M3 Ultra, M4, etc. all support Metal 3.1
16+
// M1 and M2 families do NOT support Metal 3.1 (bfloat16 in shaders)
17+
std::string s(chip);
18+
if (s.find("M4") != std::string::npos) return true;
19+
if (s.find("M3") != std::string::npos) return true;
20+
// M1, M2, and anything else: not supported
21+
return false;
22+
}
823

924
// =============================================================================
1025
// LOCAL-FIRST CONFIGURATION
@@ -83,6 +98,10 @@ bool MetalRTLoader::is_local_mode() {
8398
return METALRT_LOCAL_BUILD || (env && env[0] != '\0') || !resolve_local_repo().empty();
8499
}
85100

101+
bool MetalRTLoader::gpu_supported() {
102+
return gpu_supports_metal31();
103+
}
104+
86105
bool MetalRTLoader::is_available() const {
87106
struct stat st;
88107
return stat(dylib_path().c_str(), &st) == 0;
@@ -97,6 +116,17 @@ bool MetalRTLoader::load() {
97116
return false;
98117
}
99118

119+
if (!gpu_supports_metal31()) {
120+
LOG_ERROR("MetalRT", "MetalRT requires Apple M3 or later (Metal 3.1). "
121+
"Your chip does not support bfloat16 GPU shaders. "
122+
"Use llama.cpp engine instead: rcli engine llamacpp");
123+
fprintf(stderr, "\n %s%sMetalRT requires Apple M3 or later.%s\n"
124+
" Your Mac uses an M1/M2 chip which doesn't support Metal 3.1 shaders.\n"
125+
" Please use llama.cpp instead: %srcli engine llamacpp%s\n\n",
126+
"\033[1m", "\033[31m", "\033[0m", "\033[1m", "\033[0m");
127+
return false;
128+
}
129+
100130
handle_ = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
101131
if (!handle_) {
102132
LOG_ERROR("MetalRT", "dlopen failed: %s", dlerror());
@@ -361,6 +391,14 @@ static bool install_from_remote(const std::string& edir, const std::string& vers
361391
}
362392

363393
bool MetalRTLoader::install(const std::string& version) {
394+
if (!gpu_supports_metal31()) {
395+
fprintf(stderr, "\n %s%sMetalRT requires Apple M3 or later.%s\n"
396+
" Your Mac uses an M1/M2 chip which doesn't support Metal 3.1 shaders.\n"
397+
" Please use llama.cpp instead: %srcli engine llamacpp%s\n\n",
398+
"\033[1m", "\033[31m", "\033[0m", "\033[1m", "\033[0m");
399+
return false;
400+
}
401+
364402
std::string edir = engines_dir();
365403
std::string mkdir_cmd = "mkdir -p '" + edir + "'";
366404
if (system(mkdir_cmd.c_str()) != 0) return false;

src/engines/metalrt_loader.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ class MetalRTLoader {
137137
static std::string engines_dir();
138138
static std::string local_repo_path();
139139
static bool is_local_mode();
140+
static bool gpu_supported();
140141

141142
static constexpr uint32_t REQUIRED_ABI_VERSION = 2;
142143

0 commit comments

Comments
 (0)