Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

今天在尝试使用llama.cpp在mac下本地运行大模型的时候发现拉取llama.cpp的仓库还是比较慢,这点真的是挺难受,可能我们在玩一项技术的时候,有一大半时间都在克服这个问题,
这个方式也是谷歌搜到的,就是走 https://gh-proxy.com/ 转换镜像链接,
比如刚才的llama.cpp的仓库,https://github.com/ggml-org/llama.cpp.git 这个地址
通过 gh-proxy的转换就变成了

1
https://v4.gh-proxy.org/https://github.com/ggml-org/llama.cpp.git

但是呢有个问题,如果是走git clone我发现这个镜像地址也会是一开始比较快,后面就变慢直到速度很慢然后失败
所以另一个方法是直接用github的打包zip下载功能
一般是这种地址

1
https://github.com/ggml-org/llama.cpp/archive/refs/heads/master.zip

关键是它也可以走gh-proxy的中转,
转换之后就可以变成了

1
https://v4.gh-proxy.org/https://github.com/ggml-org/llama.cpp/archive/refs/heads/master.zip

这里的差别主要是git clone会按文件传,本身仓库会有很多小文件,所以通过压缩包的形式,相对来说通过分块传输会效率更高一些
至于今天本来想玩的本地模型,发现27B在mac上的确是不太跑得起来,甚至有点想买个512g或者256g的mac studio,不知道啥时候会出新款
苹果有没有可能财大气粗不受内存涨价的影响呢

上次我是实地使用llama.cpp跑了本地大模型,这种测试成本比较大,
刚好上次看到有个软件可以运行在本地来评估当前机器的性能可以跑什么样的大模型
它就是llmfit
在mac可以使用

1
brew install llmfit

windows可以

1
scoop install llmfit

安装好就可以直接运行了

可以看到有列了很多模型,包括参数量,token速度,占用磁盘大小,内存占用率,上下文大小等等
这样就很容易能看到我的电脑能跑什么样的模型,另外比较重要的是内存和硬盘的占用,
因为当我们真正使用的时候,除非这个电脑就被定位是只用来跑模型,不然我们一般也还会有同时在电脑上运行的程序
比如要一边写点文章,看会视频等,比如占用个50%的内存那差不多,如果要90%这种,显然是不太实际的
还有上下文长度,如果要运行对应的agent的话,还需要保障基础的上下文长度
它的原理主要是通过硬件驱动接口来识别当前设备的配置情况
比如n卡的话就是nvidia-smi,

1
2
3
4
5
6
llmfit在启动时使用sysinfo(用于RAM和CPU)和特定于供应商的工具组合读取你的系统规格:

NVIDIA:查询nvidia-smi,为多GPU设置聚合所有检测到的GPU的显存
AMD:通过rocm-smi检测
Intel Arc:从sysfs读取独立显存,通过lspci集成
Apple Silicon:通过system_profiler读取统一内存(显存 = 系统RAM,因为是共享池)

它还识别正在使用的加速后端——CUDA、Metal、ROCm、SYCL或CPU(ARM/x86)——,因为这直接影响速度估计。

除了购买各种订阅和api服务,还有一种选择就是本地运行模型,但是这个一般来讲还是只能运行一些参数量比较小的,
随着开源权重模型的发展,这个方向也在慢慢的改变,当然差距肯定是有的,只是本地我们可以用来做些辅助工作
之前很多情况下一般都只是能作为玩具,并且由于ollama和lm studio还是有一些性能损耗
之前有了解到llama.cpp这个开源项目,貌似很多lm studio等都是基于它构建的,那么直接用它是不是可以更充分的压榨我的渣渣显卡性能
首先可以在llama.cpp的github地址下载已经构建好的包
比如我是windows下,然后是在笔记本上的3060(6g)显卡,
cuda版本可以通过nvidia-smi查看,我的是12版本的

注意这里要下载两个包,一个是llama的主包,还有是后面跟着的[CUDA 12.4 DLLs]也是得下载的,不然会当成cpu模式在运行
后面的包解压后也放在前面主包解压的目录里,
这里主要是看下怎么设置参数,因为像之前说的,我有在本地运行hermes agent,它最低需要64k的上下文,那么我就是想试试哪个模型可以
看了下模型体积,只能上9B参数量左右的模型,否则都是直接爆显存
所以就试下qwen3.6 的 9B模型,可以在hugging face下载模型

1
.\llama-cli.exe -m F:\models\Qwen3.5-9B-Q4_K_M.gguf --ctx-size 65536 --flash-attn on --cache-type-k q4_0 --cache-type-v q4_0 --n-gpu-layers 28 --parallel 1

用的是 Q4_K_M 量化的,还是相对折中的,
--ctx-size 65536 代表上下文是64k,符合爱马仕的要求
--flash-attn on 是开启 Flash Attention,也是为了降低 attention 计算的显存占用
--cache-type-k q4_0 --cache-type-v q4_0 是设置了kv cache的量化大小,也是进行适当降低,防止显卡扛不住
--n-gpu-layers 26 是加载到显存26层,因为本身64k的上下文已经很占显存,所以只能加载26层,否则就会报显存oom这种
--parallel 1 表示并行度是1,因为我就单个会话使用
llama-cli.exellama-server.exe 的差别是你就在运行的窗口对话还是需要提供对话服务给Open WebUI、Cherry Studio 这些使用
当然也可以是爱马仕
直接cli运行的话,我测了下,能有6.6token/s, 勉强还可以使用的感觉,这样是占了5.2g的显存,并且如果模型用来做一些简单任务的话,还是比较可用的

一段时间都有在用大模型来辅助编程,不过现在很多都叫成vibe coding了,连胡彦斌都在vibe coding了,
现在是个全民皆可做软件开发的时代,包括像之前那个小猫补光灯,有的人的想法是说点子不重要了
我自己想的有点不一样,vibe coding现在是把成熟的开发者和小白或者完全不了解代码的人之间的差距磨平或者说缩小很多了
一旦有点子,很快就可以让大模型来做想法的实现,随着模型能力越来越强,
以前需要一定的开发知识来搞懂怎么建项目环境,构建,打包这些,都变得完全可以由大模型来处理
那么剩下的是什么呢,
一方面我理解还是对于把开发作为工作的软件工程师来说,对项目历史,背景,包袱的理解
这个很大程度也有很多人都在尝试通过SDD等方式来解决,但是目前看下来还是有一定差距,取决于项目背景的复杂度
还有一方面是做的事情是独立的还是需要多方合作,比如有个项目,涉及到四方合作开发,并且是一个流程上的上下游
你只是作为其中一个环节的开发人员,那么这个开发本身可能是问题不大,但是对于上下游的不可控,还是需要人力做比较大的介入和兜底
这里都基于一个是模型已经是比较强的情况下,如果模型是比较弱的
还需要考虑这个基础的上下文能让模型去理解,这里就有个很大的差异点,需要我们把任务,包括它的背景,上下文,项目的结构,代码逻辑都能够
作为prompt和rules等输入给模型,让它能产出质量比较好的交付代码,这个点我理解对使用者本身的要求是比较高的
并且需要进行反复的尝试纠错优化,包括结合模型能力,上下文大小等等
最终就是不同的人能对模型能力的放大或者缩小作用
我前面会用一句话来提出一些需求让大模型去开发
这些其实都是一些类似于todo工具这样网上一大堆代码样例,大模型本身可能已经学到过
但是如果是一些非常复杂包含很多背景知识和条件框架限制的,很重要的就是要把这些表达清楚
有条理,不能再试一句话需求,但也不能赘述太多撑爆上下文
再往深入纠缠下就是人如果能把一个复杂度1000的任务,拆解上12个复杂度100的任务,让模型来完成,然后再结合起来跑通这个复杂度1000的任务
也就是目前很牛的一个状态了,这里想的深入点跟架构师的能力也是类似的,首先要把这个难的任务做分解,那分解的依据就是架构本身
怎么去拆解子模块,子模块本身要完成什么目标,子模块之间要怎么衔接,非常考验人的架构能力
说到最后这都有个隐藏的前提,就是模型还未强大到面对各种复杂的任务都只要一句话就能完成
如果有那么一天,我理解可能大家都要退休了吧

偶尔下载一些视频媒体的时候会想要对它进行压缩来缩小占用空间,
但是如果每次等下载完再去用软件压缩会比较麻烦
这里就找到了inotify扩展,可以监听文件变更
这里其实想吹一下小米的mimo2.5模型,因为要装php扩展,
我的是php7.4,网上搜了下有基于pecl,有的是基于源码
我就直接丢给配了小米模型的hermes agent来安装,直接就给我装好了
顺带再帮我写了个artisan命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class InotifyWatchCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'file:watch
{path : Directory to watch}
{--recursive : Watch subdirectories recursively}
{--event=* : Events to watch (create, modify, delete, move). Default: create,modify}
{--ext=* : Only watch files with these extensions (e.g., --ext=mp4 --ext=mkv)}
{--output= : Output file path for logging (optional)}
{--format=text : Output format: text or json}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Watch directory for file write events using inotify';

/**
* Event constants mapping.
*/
protected $eventMap = [
'create' => IN_CREATE,
'modify' => IN_MODIFY,
'delete' => IN_DELETE,
'move' => IN_MOVED_TO,
'close_write' => IN_CLOSE_WRITE,
'open' => IN_OPEN,
'access' => IN_ACCESS,
'attrib' => IN_ATTRIB,
];

/**
* @var resource
*/
protected $inotify;

/**
* @var array
*/
protected $watchDescriptors = [];

/**
* @var resource|null
*/
protected $outputHandle;

/**
* @var bool
*/
protected $running = true;

/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$path = trim($this->argument('path'));
$recursive = $this->option('recursive');
$events = $this->option('event') ?: ['create', 'modify'];
$extensions = $this->option('ext');
$outputFile = $this->option('output');
$format = $this->option('format');

// Validate path
if (!is_dir($path)) {
$this->error("Directory does not exist: {$path}");
return 1;
}

// Setup output file if specified
if ($outputFile) {
$this->outputHandle = fopen($outputFile, 'a');
if (!$this->outputHandle) {
$this->error("Cannot open output file: {$outputFile}");
return 1;
}
}

// Calculate event mask
$eventMask = 0;
foreach ($events as $event) {
if (!isset($this->eventMap[$event])) {
$this->error("Unknown event: {$event}. Available: " . implode(', ', array_keys($this->eventMap)));
return 1;
}
$eventMask |= $this->eventMap[$event];
}

// Always add IN_CLOSE_WRITE for reliable write detection
$eventMask |= IN_CLOSE_WRITE;

// Initialize inotify
$this->inotify = inotify_init();

if (!$this->inotify) {
$this->error("Failed to initialize inotify");
return 1;
}

// Set non-blocking mode for graceful shutdown
stream_set_blocking($this->inotify, false);

// Setup signal handling for graceful shutdown
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGTERM, [$this, 'handleSignal']);
pcntl_signal(SIGINT, [$this, 'handleSignal']);
}

// Add watches
$this->addWatch($path, $recursive);

$this->info("Watching: {$path}");
$this->info("Events: " . implode(', ', $events));
$this->info("Recursive: " . ($recursive ? 'yes' : 'no'));
if ($extensions) {
$this->info("Extensions: " . implode(', ', $extensions));
}
$this->info("Press Ctrl+C to stop");
$this->line('');

// Main loop
$checkInterval = 500000; // 500ms in microseconds

while ($this->running) {
// Check for signals
if (function_exists('pcntl_signal_dispatch')) {
pcntl_signal_dispatch();
}

$events = inotify_read($this->inotify);

if ($events !== false && !empty($events)) {
foreach ($events as $event) {
$this->handleEvent($event, $extensions, $format);
}
}

usleep($checkInterval);
}

// Cleanup
$this->cleanup();

return 0;
}

/**
* Add inotify watch for a directory.
*/
protected function addWatch(string $path, bool $recursive): void
{
$mask = IN_CREATE | IN_MODIFY | IN_DELETE | IN_MOVED_TO | IN_CLOSE_WRITE | IN_OPEN | IN_ATTRIB;
$wd = inotify_add_watch($this->inotify, $path, $mask);
$this->watchDescriptors[$wd] = $path;

if ($recursive) {
$items = scandir($path);
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$fullPath = $path . DIRECTORY_SEPARATOR . $item;
if (is_dir($fullPath)) {
$this->addWatch($fullPath, true);
}
}
}
}

/**
* Handle inotify event.
*/
protected function handleEvent(array $event, array $extensions, string $format): void
{
$wd = $event['wd'];
$mask = $event['mask'];
$name = $event['name'] ?? '';
$cookie = $event['cookie'] ?? 0;

// Get directory path from watch descriptor
$dir = $this->watchDescriptors[$wd] ?? 'unknown';

// Skip if no filename
if (empty($name)) {
return;
}

// Filter by extension if specified
if (!empty($extensions)) {
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
if (!in_array($ext, $extensions)) {
return;
}
}

// Determine event type
$eventType = $this->getEventType($mask);

// Build file path
$filePath = $dir . DIRECTORY_SEPARATOR . $name;

// Build event data
$data = [
'timestamp' => date('Y-m-d H:i:s'),
'event' => $eventType,
'file' => $name,
'path' => $filePath,
'dir' => $dir,
'cookie' => $cookie,
];

// Output based on format
if ($format === 'json') {
$output = json_encode($data, JSON_UNESCAPED_UNICODE) . "\n";
} else {
$output = $this->formatTextOutput($data);
}

// Write to console
$this->writeToConsole($eventType, $output);

// Write to file if handle exists
if ($this->outputHandle) {
fwrite($this->outputHandle, $output);
}
}

/**
* Get human-readable event type from mask.
*/
protected function getEventType(int $mask): string
{
$types = [];

if ($mask & IN_CREATE) {
$types[] = 'CREATE';
}
if ($mask & IN_MODIFY) {
$types[] = 'MODIFY';
}
if ($mask & IN_DELETE) {
$types[] = 'DELETE';
}
if ($mask & IN_MOVED_TO) {
$types[] = 'MOVED_TO';
}
if ($mask & IN_CLOSE_WRITE) {
$types[] = 'CLOSE_WRITE';
}
if ($mask & IN_OPEN) {
$types[] = 'OPEN';
}
if ($mask & IN_ACCESS) {
$types[] = 'ACCESS';
}
if ($mask & IN_ATTRIB) {
$types[] = 'ATTRIB';
}

return implode('|', $types);
}

/**
* Format text output.
*/
protected function formatTextOutput(array $data): string
{
return sprintf(
"[%s] %-12s %s (%s)\n",
$data['timestamp'],
$data['event'],
$data['file'],
$data['dir']
);
}

/**
* Write output to console with color coding.
*/
protected function writeToConsole(string $eventType, string $output): void
{
if (strpos($eventType, 'CREATE') !== false) {
$this->info(rtrim($output));
} elseif (strpos($eventType, 'DELETE') !== false) {
$this->error(rtrim($output));
} elseif (strpos($eventType, 'MODIFY') !== false || strpos($eventType, 'CLOSE_WRITE') !== false) {
$this->comment(rtrim($output));
} elseif (strpos($eventType, 'MOVED_TO') !== false) {
$this->warn(rtrim($output));
} else {
$this->line(rtrim($output));
}
}

/**
* Handle signal for graceful shutdown.
*/
public function handleSignal(int $signal): void
{
$this->running = false;
$this->line('');
$this->info("Received signal {$signal}, shutting down...");
}

/**
* Cleanup resources.
*/
protected function cleanup(): void
{
if ($this->outputHandle) {
fclose($this->outputHandle);
}

// Remove all watches
foreach ($this->watchDescriptors as $wd => $path) {
inotify_rm_watch($this->inotify, $wd);
}

if (is_resource($this->inotify)) {
fclose($this->inotify);
}

$this->info("Watch stopped.");
}
}

只需要执行命令
php artisan file:watch /home/nick/listen --recursive
然后我新建一个文本文件

1
touch 1.txt

就能看到文件的变更

这个流程下来我都没有任何调整,都是一步到位了
这个因为没有准确的条件对比,可能是hermes也起了一定作用,还是说本身模型就很强
主要是监听了文件的创建,修改,关闭写入等,
首先是inotify_init函数 — 初始化 inotify 实例,类似于客户端
然后是inotify_add_watch 函数 — 添加监听到已初始化的 inotify 实例

1
2
3
4
5
6
7
8
inotify_add_watch(resource $inotify_instance, string $pathname, int $mask): int|false
参数 ¶
inotify_instance
inotify_init()返回的资源
pathname
要监听的文件或目录
mask
监听事件。详情见 预定义常量。

具体的预定义常量有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
IN_ACCESS (int)
文件被访问(读)(*)

IN_MODIFY (int)
文件被修改(*)

IN_ATTRIB (int)
元数据变更(例如:权限、修改时间等)(*)

IN_CLOSE_WRITE (int)
打开写入后关闭的文件(*)

IN_CLOSE_NOWRITE (int)
未打开写入的文件被关闭(*)

IN_OPEN (int)
文件被打开(*)

IN_MOVED_TO (int)
文件被移动到监听目录(*)

IN_MOVED_FROM (int)
文件移出监听目录(*)

IN_CREATE (int)
在监听目录中创建文件或文件夹(*)

IN_DELETE (int)
监听目录中删除文件或文件夹(*)

IN_DELETE_SELF (int)
监听的文件或目录被删除

IN_MOVE_SELF (int)
IN_CLOSE (int)
与 IN_CLOSE_WRITE | IN_CLOSE_NOWRITE 相等

IN_MOVE (int)
与 IN_MOVED_FROM | IN_MOVED_TO 相等

IN_ALL_EVENTS (int)
以上所有常量的位掩码

IN_UNMOUNT (int)
卸载包含监听对象的文件系统

IN_Q_OVERFLOW (int)
事件队列溢出(此事件中监听描述符是 -1)

IN_IGNORED (int)
监听被移除(通过 inotify_rm_watch() 显示移除,或者是文件被移动,或者文件系统被卸载)

IN_ISDIR (int)
事件发生的主体是目录

IN_ONLYDIR (int)
如果是目录,仅监听路径名(从 Linux 2.6.15 开始)

IN_DONT_FOLLOW (int)
如果是符号链接,不引用路径名(从 Linux 2.6.15 开始)

IN_MASK_ADD (int)
如果路径名存在,追加监听此路径的事件掩码(代替更换掩码)

IN_ONESHOT (int)
路径触发一个监听事件后,从监听列表中移除。

接下去是

1
inotify_read(resource $inotify_instance): array

会返回以下内容

1
2
3
4
wd 是由 inotify_add_watch() 返回的监听描述符
mask 是 events 的位掩码
cookie 是连接相关事件(例如:IN_MOVE_FROM 和 IN_MOVE_TO)的唯一 id
name 是文件名(例如:监听目录中被修改的文件)

注意这个返回值

1
返回 inotify 事件数组。在没有待处理事件或 inotify_instance 非阻塞时返回 false。事件发生时都会返回包含以下键的数组:

如果要设置非阻塞的话

1
stream_set_blocking($fd, 0);

$fd就是init返回的inotify对象
如果是非阻塞就是自己循环去read有没有事件,
如果是阻塞的话就是read会一直等到有事件了才返回
还可以通过

1
inotify_queue_len(resource $inotify_instance): int

如果有数量代表read不是阻塞的,有待处理的事件

最后还有通过inotify_rm_watch来清除监听器

1
inotify_rm_watch(resource $inotify_instance, int $watch_descriptor): bool

这样针对文件系统有变更的话就会比较方便去做下一步处理,
比如压缩,移动归档等
之前最开始的处理方式是每次都遍历,感觉还是挺不科学的,
已有的文件会一遍遍被重复扫描
这个方法还有个局限性就是得在Linux系统里,
其他系统例如mac需要fs_usage这些工具才行
Windows里需要使用 ReadDirectoryChangesW 等api
总结下来这个方式对于Linux系统下的ext4系统是比较方便的,
其他的系统可能需要再研究下

0%