2025-03-24 13:11:58
最近买了个 8845HS 的小主机,但因为 780m 的显卡并没有被 ROCm 列为官方支持的卡,所以目前需要很多 trick 来运行
最主要的就是通过 HSA_OVERRIDE_GFX_VERSION
来假装成受支持的显卡。虽然我用的都是 HSA_OVERRIDE_GFX_VERSION=11.0.2 但实际上因 ROCm 版本的不同,到底哪个能在你的显卡上工作需要自己测试下。
你可以通过 AMD 官网的 Supported GPUs 中的 Architecture 和 LLVM target 来查找,比如 gfx1101,那就是 HSA_OVERRIDE_GFX_VERSION=11.0.1
除此之外,ls /opt/rocm/lib/rocblas/library
命令也会列出一些没显示在官网上的支持,比如 11.0.2
ROCm 是 AMD 显卡玩机器学习的基础组件,现在安装起来很简单,amdgpu-install 这个包就可以很好的解决
sudo apt install amdgpu-install
amdgpu-install --usecase=rocm
Ollama 的运行可以参考下面的 PR Enable AMD iGPU 780M in Linux, Create amd-igpu-780m.md #5426
简而言之,直接通过这个命令就可以运行
HSA_OVERRIDE_GFX_VERSION=11.0.2 ollama serve
目前 Ollama 对 igpu 的显存支持有些问题,不能够将所有共享内存计算在内,解决方案可以参考这里 AMD integrated graphic on linux kernel 6.9.9+, GTT memory, loading freeze fix #6282
我的主要场景并非用 8845HS 来跑 LLM,所以就跑个简单的测试下吧,gemma3:12b 这个量化模型的速度大概是 8.44 tokens/s 可以说堪用
使用 ollama ps
命令可以查看模型是分配在哪个设备上运行的。
PyTorch 直接通过AMD 官网提供的命令来安装即可,需要注意的是我使用 PyTorch 官网的命令安装稳定版并不能成功运行,AMD 官网给出的 nightly 版本可以。
pip3 install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/rocm6.2.4/
同样,需要使用 HSA_OVERRIDE_GFX_VERSION=11.0.2
来运行,可以创建一个 .env 文件在 ipynb 里动态载入,如果是 vscode 的话,.env 文件会自动加载,不需要下述步骤
pip install python-dotenv
在 ipynb 顶部加入一个 code block 每次运行一下即可
%load_ext dotenv
%dotenv
其它内容无需修改
配置完成后,如果有时候不确定有没有跑在 GPU 上,可以用 radeontop 来监控
sudo apt install radeontop
8845HS 还带了个 16 TOPS 的 NPU,不过要等到 Linux 6.14 才会合并进去。
届时 ONNX Runtime 的 VitisAIExecutionProvider 和 HuggingFace 的 RyzenAI 应该都能开箱即用。
唯一的问题是兼容性如何。
暂时还没折腾,等到时候也会测试一下再写一篇折腾的博客
Does ROCm 5.7 support Radeon 780M (gfx1103)? #2631
Feature: ROCm Support for AMD Ryzen 9 7940HS with Radeon 780M Graphics #3398
2025-03-20 16:25:56
最近经常想起自己为什么离开公司自己做产品,都已经是 7 年前的事情了
当时为了能够按照自己的意志去做产品
做一款足够独特,有趣的产品
这些年事情虽然做的慢了些
但现在对这个目标有了更好的想法,更成熟的思考
7 年间发生了很多事情,失去了挚友
身份,家庭,关系也都在变化
但今天我真的迈入新的副本了
副本开始前,是应该好好休息一下的!
2025-03-08 11:07:40
昨天看介绍说乔布斯在设计苹果的新总部时希望有在公园里办公的感觉,因为在亲近自然的时候最有灵感。
想起了去年住的东越谷,东越谷是真的挺美的,河道,公园,家里的窗外。
因为不能养猫所以搬家了,离开了这个环境后经常会想念,所以也在考虑是不是应该再搬家,找一个能养猫又亲近自然的地方。
但还是等春天来了之后,看看这里万物复苏后是什么感觉,或许也会很美。
最近集中看了一些电影,「怪物」「去唱卡拉OK吧」「爱的接力棒」这些
感觉也许是自己真的步入中年,远离青春了
「去唱卡拉OK吧」是那种后劲很足的青春感
而且不是那种恋爱的青春
2024-12-07 18:40:02
11 月中旬的时候,Andy 私信问我愿不愿意体验一下懒猫微服这款产品,我看了下懒猫微服的官网,然后几个疑问就冒了出来
于是我对 Andy 说,关注我的人应该大都懂技术,对参数也比较敏感,可能找一些数码博主来体验会更好。
但 Andy 还是说,你先体验下,如果喜欢的话就点评点评。
好吧,既然条件这么宽松,我也很好奇原 Deepin Linux CTO 带来的产品,会有什么不同,在这些问题中,我最好奇的是懒猫的网络穿透技术,是不是真的能构建出有用的使用场景。
因此本次的体验将跳过所有和外观以及参数相关的内容,只在于使用场景的探索。
懒猫到手后配置的流程很简单,打开 App 扫机器底部的二维码,几个下一步就可以配置好,和 HomeKit 的智能设备入网差不多。这个流畅简单的体验是让我觉得有些惊喜的,和我之前用的 NAS 相比是非常新手友好了。
与传统 NAS 不同的是,你暂时无法直接通过一个局域网的地址访问懒猫微服,想要正常使用懒猫,都需要启动他们的 App 进行组网,然后通过 App 管理和访问懒猫的功能。
比较省心的是,不论你身处何地,都可以通过这个懒猫微服的内网穿透能力,无缝的访问家里懒猫微服的应用和内容。
原理上来说,懒猫微服会通过内网穿透组网的能力,将你登陆了懒猫 App 的设备和你家里的懒猫微服组合到一个加密的私有网络里,在这之后,懒猫 App 会接管你设备上所有访问 heiyu.space 域名后缀的流量,而每个你安装的懒猫上的应用,都会有对应的 heiyu.space 域名,比如懒猫清单对应的就是 https://todolist.myoland.heiyu.space
当你访问这个域名的时候,无需经过第三方中转即可直连家中的懒猫微服,这意味着你既不需要租服务器,处理那些烦人的备案合规问题,也不需要担心带宽和流量问题,更无须担心自己的数据会被窥窃。
如果这个域名只有自己能访问,那就相当无趣了,懒猫让我开始觉得兴奋的地方是,你可以邀请最多 20 名用户加入到你的微服上,这意味着,虽然他们没有微服设备,但可以访问,操控,管理你的懒猫微服和上面的应用。
在看到懒猫这种便利的组网能力后,我立刻想起了我之前和团队成员协作时一直在用的 Mattermost,一个 Slack 的开源替代品,出于数据安全的考虑,我并不想使用国内的协作产品聊天,但我又需要大家能稳定的连接到这个服务上,一个架设在自己家里的 Mattermost 就是很好的选择。
可惜在我使用的时候懒猫微服的应用商店还没有上架这个应用。
好吧,自己动手丰衣足食,这也是增进对懒猫了解的好机会,我着手移植了一个懒猫微服版本的 Mattermost,在这篇 Blog 发布的日期,我已经提交 Mattermost 到懒猫的应用商店,如果你想参考移植自己的项目,可以在这里找到项目代码 https://github.com/kevinzhow/mattermost-lzc
Mattermost 在安装后你可以访问 https://mattermost.myoland.heiyu.space 来打开 Mattermost,也可以直接通过这个域名在 App 上登录。
得益于懒猫 App 在电脑端不错的兼容性,懒猫微服不会和你电脑上其他的 VPN 服务冲突,Mattermost 用起来也非常无缝,在一些网络配置后,远程协助和电话功能也都能正常运行,这一刻我有些爱上了懒猫微服为团队协作提供的便利性。
团队协作第二个最常见的需求就是点对点文件同步,以前一直用 Resilio 进行同步,但是感觉不论是权限管理还是后来的改动,都让我用的有些不开心,这次我尝试了 Syncthing 这款开源产品。
虽然懒猫的应用商店自带了 Syncthing 但是有两个问题导致我还是自己进行了一次移植
你可以在这里找到我的移植项目 https://github.com/kevinzhow/syncthing-lzc
因为一开始我带入了 NAS 的概念,所以看到硬盘 1 和 硬盘 2 的使用量一样时,我以为是组了 RAID,但客服告诉我这是 btrfs 均匀存储的特性,多年 Mac 用户此刻表示受到了开源震撼。
我另外一台 NAS 虽然用了 RAID 技术,但比起 RAID 让人摸不清头脑的规格,我个人更喜欢懒猫这种外接一个 USB 硬盘进行增量数据备份的方式,机器因此可以做小,更省电,自己也可以更自由的管理移动备份硬盘。
其实在找到了团队协作这个场景之后,我认为懒猫微服已经给我提供了一个非常独特的使用场景,从一开始带着偏见的怀疑,变成了希望他们做大做强。
正如它名字里体现的一样,这是一个开箱即用的私有云服务主机,NAS 和 AI 都不是它打动我标签,懒猫微服隐藏了 NAS 产品过去的复杂细节,将微服这个概念交付到了用户手中。
希望不折腾,拥有开箱即用的私有云服务主机的人。 另外懒猫很快会推出海外版本,相信届时如果结合 Cloudflare Tunnel,这应该也会是一个非常好的公网服务器。
2024-10-10 18:47:53
这次搬家是彻底失算了,没想到日本 2022 年的新筑 Terrace (日语叫 メゾンネット,中文可以理解为联排)竟然没有光纤,这就导致在各家装宽带的网页上,公寓和一户建都不太能准确描述这个地方。
各种波折导致我没能顺利搬迁之前的宽带,也没能很快装上新的宽带。这也是为什么我会写这篇文章,主要是抒发下没有宽带的苦闷。
日本很多运营商都提供了一种叫做 Home Wi-Fi 的家庭热点,原理和手机热点一样,都是通过一张 SIM 卡连接到移动网络,然后再分享出来。
但与手机不同的是,Home WI-FI 这种设备把技能点都点在了下载上。
比如在我这里通过 5G 连接 Au 的线路,下载可以到 200M,但上传可能只有 10M - 20M,同样的卡切换成我的 Pixel 7a 之后,下载是 120M 左右,上传是 60 - 80M. 下图可以看到 Pixel 7a 的上传性能要好很多。
有了这个数据的对比后,我决定把 Android 手机的网通过 RJ45 网线提供给路由器上网。
在日本 Android 手机只能分享 2.4G 的 WI-FI,这导致 WI-FI 的速度和稳定性,带机量都相当有限。
具备下面 3 个物品就可以方便的自制一个 Home WI-FI 了
在日本的话 Povo 和乐天这两个放题卡都是不错的选择,需要注意的是自己所在的区域是否有对应的 5G 信号支持,5G 通常可以提供一个 20ms 左右的低延迟体验,即使人多的时候速度不会降的离谱。
虽然 小米,三星,Google Pixel 的手机大都支持以太网网络共享,但这一点还是需要自己先确认下。
你可以选择一个带有 Type C 供电的转接头,比如我购买的支持 PD 电源输入的这款
除了专门买个转接头外,也可以用自己手头带有 RJ45 接口的扩展坞,但扩展坞和手机的兼容性可能不太好,比如我手里的 Anker 扩展坞可以和小米手机一起用,但 Pixel 使用则会一直掉线。
在购买前需要确认转接设备和 Android 手机的兼容性,一般如果商家写了支持 Android 设备那么通常没问题。
正常的家用 WI-FI 路由器即可 :)
好了,可以享受网络了!我用了一个星期后感觉还是挺稳定的,不过还是祝你早日装上宽带!
2023-10-30 13:28:30
在上一章中我们创建了 User
的 Model
,并构建了 UserController
,但尚未构建起 User
和 Post
之间的关系。在这一章中,我们将在 Vapor 中完成一对多的关系构建。
本章代码可以在 Github 中找到。
要修 Post
表结构,我们首先需要创建一个新的 Migration 文件。
创建 Sources/App/Migrations/3_AddUserIDToPost.swift
文件,添加如下代码:
import Fluent
struct AddUserIDToPost: AsyncMigration {
func prepare(on database: Database) async throws {
try await database.schema(Post.schema)
.field("user_id", .uuid, .references(User.schema, "id"))
.update()
}
func revert(on database: Database) async throws {
try await database.schema(Post.schema)
.deleteField("user_id")
.update()
}
}
这种方式将会给 Post 表添加一个可为空的 user_id
字段,虽然这种方式简单直接,但同时也意味着,我们容忍了 Post 可能不属于任何用户的情况。
在 prepare
方法中,我们使用 references
方法来创建了一个外键约束,这个外键将会引用 User
表中的 id
字段,确保了 user_id
在数据库层面的正确性,避免我们意外插入一个不存在的 user_id
。
在 revert
方法中,我们使用 deleteField
方法来删除 user_id
字段,这个方法会同时删除它的外键约束。
随后,我们应当修改 configure.swift
将这条 Migration 添加到其中
app.migrations.add([CreatePost(), CreateUser(), AddUserIDToPost()])
现在,运行 vapor run migrate
即可进行数据库的迁移。
完成了数据库 Schema 的修改后,我们需要进一步修改 Post Model
编辑 Sources/App/Models/Post.swift
文件如下:
final class Post: Model, Content {
static let schema = "posts"
@ID(key: .id)
var id: UUID?
@OptionalParent(key: "user_id")
var user: User?
@Field(key: "content")
var content: String
@Timestamp(key: "created_at", on: .create)
var createdAt: Date?
init() { }
init(id: UUID? = nil, content: String) {
self.id = id
self.content = content
}
}
因为我们在给 Post 的 Schema 添加 user_id
字段时允许其为空,因此在 Post Model 中,我们使用 @OptionalParent
来表明 user
是不一定存在的。
与此同时,@OptionalParent(key: "user_id")
表明了 user_id
和 User Model @ID 之间的对应关系,这与 Schema 中的 .field("user_id", .uuid, .references(User.schema, "id"))
形成对应关系。
import Vapor
import Fluent
final class User: Model, Content {
static let schema = "users"
@ID(key: .id)
var id: UUID?
@Field(key: "username")
var username: String
@Field(key: "password_hash")
var passwordHash: String
@Children(for: \.$user)
var posts: [Post]
@Timestamp(key: "created_at", on: .create)
var createdAt: Date?
init() {}
init(id: UUID? = nil, username: String, passwordHash: String) {
self.id = id
self.username = username
self.passwordHash = passwordHash
}
}
在 User Model 中,@Children(for: \.$user)
表明了 User 和 Post 之间的一对多关系,同时也说明了,这种关系是通过 Post Model 中的 user
属性来进行关联的。
现在,我们有两种方式可以创建 Post 了,一种是通过 Post Model 的 user
属性:
let user = User(username: "hellouser", passwordHash: try await app.password.async.hash("123456"))
try await user.save(on: app.db)
let post = Post(content: "Hello, World!")
post.$user.id = try user.requiredID()
另一种是通过 User Model 的 posts
属性
let user = User(username: "hellouser", passwordHash: try await app.password.async.hash("123456"))
try await user.save(on: app.db)
let post = Post(content: "Hello, World!")
user.$posts.create(post, on: app.db)
不管采用哪一种,我们都需要在保存 Post 前知道 user_id
。
在 PostController 中,我们需要修改 create
方法,使其能够接收 user_id
参数,并将其与 Post 关联起来。
那么如何将 user_id
传入到 create
方法中呢?
直接让客户端提供 user_id
参数肯定是不可以的,因为这样的话,客户端只需要伪造 user_id
就可以假装成别的用户发布,这显然是不安全的。
我们暂且通过最简单的 BasicAuthorization
来解决这个问题,BasicAuthorization 需要客户端提供 username 和 password 两个字段,并将其进行 Base64 编码后放在 HTTP Header 中:
计算格式如下
base64(username:password)
以上面的用户信息 happyuser:123456
为例,计算后的 Header 结果如下:
Authorization: Basic aGFwcHl1c2VyOjEyMzQ1Ng==
服务器端验证用户名和密码后,才会允许客户端创建对应用户的 Post。
BasicAuthorization 作为一种业界通用实践,Vapor 已经提供了 Model Authenticatable 来帮助我们完成这个功能
修改 Sources/App/Models/User.swift
在底部增加如下代码:
extension User: ModelAuthenticatable {
static let usernameKey = \User.$username
static let passwordHashKey = \User.$passwordHash
func verify(password: String) throws -> Bool {
try Bcrypt.verify(password, created: self.passwordHash)
}
}
usernameKey
和 passwordHashKey
分别表明了 User Model 中的哪两个属性用于存储用户名和密码。当客户端提供用户名和密码时,Vapor 会自动将其与 User Model 中的 usernameKey
和 passwordHashKey
进行对应,从而完成用户验证。
接下来我们需要确保 Post 在创建前,用户已经通过了验证,这可以通过在 PostController 的路由中,加入权限验证相关的 Middleware 来完成,Vapor 已经提供了 User.authenticator()
来帮助我们完成 BasicAuthorization 验证的功能。
修改 Sources/App/Controllers/PostController.swift
中 boot 代码如下
func boot(routes: RoutesBuilder) throws {
routes.group("posts") { posts in
posts.get(use: index)
posts.grouped(User.authenticator()).post(use: create)
}
}
在 posts
路由组中,当客户端请求 posts
路由组中的 post
方法前,User.authenticator()
会进行 BasicAuthorization 验证,如果没有通过验证,Vapor 会返回 401 错误。
修改 Sources/App/Controllers/PostController.swift
中的 create
方法如下:
func create(req: Request) async throws -> Post {
let user = try req.auth.require(User.self)
let postData = try req.content.decode(Post.CreateDTO.self)
let post = Post(content: postData.content)
post.$user.id = try user.requireID()
try await post.create(on: req.db)
return post
}
在 create
方法中,我们首先通过 req.auth.require(User.self)
获取到了已经通过验证的用户,然后通过 req.content.decode(Post.CreateDTO.self)
获取到了客户端传入的 Post 数据,最后通过 post.$user.id = try user.requireID()
将 Post 和 User 关联起来。
修改 Tests/AppTests/PostTests.swift
如下:
@testable import App
import XCTVapor
final class PostTests: XCTestCase {
var app: Application!
override func setUp() async throws {
app = Application(.testing)
try configure(app)
try await app.autoRevert()
try await app.autoMigrate()
}
override func tearDown() async throws {
app.shutdown()
}
func testCreatePost() async throws {
let user = User(username: "hellouser", passwordHash: try await app.password.async.hash("123456"))
try await user.save(on: app.db)
let postDTO = Post.CreateDTO(content: "Post created from test")
try app.test(.POST, "posts", beforeRequest: { req in
try req.content.encode(postDTO)
req.headers.basicAuthorization = BasicAuthorization(username: user.username, password: "123456")
}, afterResponse: { res in
XCTAssertEqual(res.status, .ok)
let post = try res.content.decode(Post.self)
XCTAssertEqual(postDTO.content, post.content)
XCTAssertEqual(try user.requireID(), post.$user.id)
})
}
}
在 testCreatePost
中,我们首先创建了一个用户,然后创建了一个 Post DTO,接着通过 app.test
方法,模拟了客户端的请求,最后验证了 Post 的 User 来判断是否创建成功。
在这一章中,我们学习了如何在 Vapor 中构建一对多的关系,同时也学习了如何使用 BasicAuthorization 来进行用户验证。但这种方式虽然简单,但也有很多缺点,在下一章中,我们会学习使用 BearerAuthentication 来对用户进行验证,并进一步修复 User Model 的安全隐患。
本章代码可以在 Github 中找到。