Skip to main content

4 posts tagged with "ai"

View All Tags

Wei Ji

前情提要

在完成了糞 Code 巡禮之後,我發現缺少一個有效的算法來探勘並解析 Git Repo,

info

關於糞 Code 巡禮請見前一篇文章:

從 Zettelize 到糞坑:評點 deepwiki-open 程式碼

於是我計畫從行之有年且可靠的傳統(經典)遊戲 AI 中尋找靈感,行為樹 (Behavior Tree) 是我的首選,但是在跟 LLM 討論的過程中它提到了 HTN (Hierarchical Task Network),於是我認為有必要稍微了解它一下。

然後經過簡單的搜索之後,我隱約判斷正確的學習路徑應該是:

  1. STRIPS (Stanford Research Institute Problem Solver)
  2. GOAP (Goal Oriented Action Planning)
  3. HTN (Hierarchical Task Network)

經過一番的學習之後,我大致理解了 STRIPS 的概念,並且寫了一篇文章紀錄:

STRIPS 學習筆記

接下來我會假設讀者已經知道 STRIPS 或是閱讀過前一篇文章而不做過多的補充,缺乏先備知識可能會難以理解以下內容。

GOAP (Goal Oriented Action Planning)

GOAP 基本上重用了 STRIPS 的問題模型,但是對 Action 加上了成本的概念,並且用 A* 演算法求解。

STRIPS 的問題本質上就是路徑規劃問題,所以用 A* 演算法求解似乎也沒什麼好意外的了。

HTN (Hierarchical Task Network)

HTN 同樣沿用了 STRIPS 中大部分概念,只是 Action 在這裡被分解了1

  • 原子任務(PrimitiveTask)
    • 基礎執行單元,不可再分解(如移動、攻擊)
    • 直接作用於世界狀態
  • 方法(Method)
    • 由多個原子任務或復合任務組合而成。
    • 包含前提條件(condition)和一組子任務(Subtasks);前提條件決定方法能否執行,子任務是具體的執行步驟
    • 按順序執行子任務(類似行為樹順序節點),任一子任務失敗則方法終止
  • 復合任務(CompoundTask)
    • 由多個方法組成的邏輯容器
    • 執行時根據條件動態選擇其中一個方法(類似行為樹選擇節點)

GOAP 的運作邏輯是先設定一堆 Action,然後給定起點與目標求解路徑規劃。 HTN 則是先設定由一堆 PrimitiveTask、Method 和 CompoundTask 構成的巨大樹,運行時則是由當前狀態排除那些不符合條件的方法,最後留下一條可行的 PrimitiveTask 路徑(Task 序列)。

接著就會依照剛剛得出的 Task 序列開始運行,直到任務完成或是運行途中實際狀態跟剛剛模擬的情況不符合造成前提條件不滿足,那就回到剛剛那個完整的樹再重新規劃一次並再次運行2

Footnotes

  1. 【遊戲演算法】遊戲AI行為決策-分層任務網路(HTN)的簡單應用 | 白雪萬事屋. Retrieved 2026-05-08, from https://www.shirakoko.xyz/article/htn

  2. 遊戲AI行為決策——HTN(分層任務網路) - 狐王駕虎 - 部落格園. Retrieved 2026-05-08, from https://www.cnblogs.com/OwlCat/p/17910900.html

Wei Ji

背景

最近在學習 STRIPS (Stanford Research Institute Problem Solver),發現中文資料很少,而且也講的不是很好理解,於是我試著將我的理解寫一篇。

低維版本

首先我想用一般人比較熟悉的向量空間來解釋。

相空間

要理解我接下來的描述,讀者需要先備相空間的概念。

如果你對「混沌理論」這個詞不陌生,你很有可能看過類似這樣的圖:

這其實就是一個相空間,每一個軸都是系統狀態的變數,因此一個點就描述了一個系統的狀態,而這些線條就是當給定一個初始狀態,這個動態系統會自然的往其他狀態移動。

第一次接觸這個概念的朋友可能會有點不好理解,因為一般人在建立向量的概念的時候通常是基於歐幾里得空間和牛頓力學之上:用向量來描述一個點的位置、速度和加速度。

「一個點會因為當下的狀態自己移動」這件事情想想有點不可思議,不過如果你這樣想:

  • 這是一個一維空間,所以一個點的座標為 x。
  • 放置另外一個質點,然後固定住它,因此剛剛放置的點會受到吸引力而獲得加速度 a。
  • 因為獲得加速度,所以點的速度 v 是會變化的。
  • v 變化了,x 就會變化;而 x 變化了,引力作用也會因此發生變化,那 a 就變化了。
  • 所以你會得到一個 x, v 和 a 都不斷變化的系統。

接著你把 (x, v, a) 當成一個三維向量在空間中畫成一張圖,就會得到一個描述系統變化的三維曲線了。

STRIPS 的模型

在 STRIPS 中,我們會定義目標和初始狀態,也就是相空間中的兩個點。

並且我會定義很多個 Action,每個 Action 有兩個特徵:

  • 向量:讓狀態在相空間移動的一個向量。
  • 條件:這個向量不是在相空間的每一個點都能使用(存在)。

用數學一點的描述方式,Action 就像是一個離散場 (Field),只是每個狀態一次只能套用其中一個離散場移動到下一個狀態。

所以問題就變成:

已知無數離散場 (Action);同時給定初始座標 (State Initial) 與目標座標 (State Goal),求:什麼樣的 Action 排列可以從初始座標移動到目標座標?

STRIPS 本質上是在處理移動手段受限的路徑搜尋問題。

高維版本

剛剛我用二維的網格解釋,接下來這是高維的版本,只是每個維度的值是二元化的。

S=(s1,s2,s3,s4,s5,s6,,sn)s=0,1S = (s_1, s_2, s_3, s_4, s_5, s_6, \dots, s_n)|_{s={0, 1}}

初始狀態可能長這樣:

Sinit=(1,0,0,0,0,0,,0)s=0,1S_{\text{init}} = (1, 0, 0, 0, 0, 0, \dots, 0)|_{s={0, 1}}

目標狀態可能長這樣:

Sgoal=(1,0,0,0,0,0,,0)s=0,1S_{\text{goal}} = (1, 0, 0, 0, 0, 0, \dots, 0)|_{s={0, 1}}

一個 Action 可能長這樣:

Ax=(Precondition,Update)=((1,0,0,0,0,0,,0)s={0,1},(1,1,0,0,0,0,,0)s={1,0,1})\begin{aligned} A_x &= (\text{Precondition}, \text{Update}) \\ &= ((1, 0, 0, 0, 0, 0, \dots, 0)|_{s=\{0, 1\}}, \\ &\quad (-1, 1, 0, 0, 0, 0, \dots, 0)|_{s=\{-1, 0, 1\}}) \end{aligned}

所以問題就變成解:

Sgoal=Sinit+AxS_{\text{goal}} = S_{\text{init}} + \sum Ax

怎樣排列 Action,才能從初始狀態變成目標狀態?

比較接近原始描述的版本

因為現實中的可能狀態非常的多,因此在 STRIPS 中狀態不是一個向量,而是一個集合:

S={s1,s2,s3,s4,s5,s6,,sn}S = \{s_1, s_2, s_3, s_4, s_5, s_6, \dots, s_n\}

存在於集合的考慮為「真」,不存在的就考慮為「假」。換句話說,命題中出現過得狀態才要考慮,沒出現的就當作不存在,即便在數學的空間中它可能存在。

因此初始狀態可能長這樣:

Sinit={"At(A)"}S_{\text{init}} = \{\text{"At(A)"}\}

目標狀態則可能長這樣:

Sgoal={"At(Z)"}S_{\text{goal}} = \{\text{"At(Z)"}\}

一個 Action 可能長這樣:

Ax=(Preconditions,Effects)=({"At(A)"},{"At(A)",+"At(B)"})\begin{aligned} A_x &= (\text{Preconditions}, \text{Effects}) \\ &= (\{\text{"At(A)"}\}, \{-\text{"At(A)"}, + \text{"At(B)"}\}) \end{aligned}

求解

STRIPS 建構了一個問題模型:

  • State, Initial State, Goal State
  • Action(Preconditions, Postconditions)
  • 已知 Actions, Initial State, Goal State
  • 求特定的 Actions 排列從 Initial State 到 Goal State

這個問題不一定有解,有解也不見得是唯一解。

好問題講完了,怎麼解?

STRIPS 本身只是建立一個問題模型,但是不包含解法。就像馬可夫鏈本身只是對問題進行數學建模,仍須透過 Q-learning 中的 value function 才能對問題求解。

至於針對 STRIPS 的具體解法我就不打算繼續深究了,因為學習 STRIPS 並不是我的目的,它只是據我了解三個算法中最簡單的那一個:

  • STRIPS (Stanford Research Institute Problem Solver)
  • GOAP (Goal Oriented Action Planning)
  • Hierarchical Task Network

HTN 才是我這次學習的目標,只是前面兩個東西似乎在觀念上構築對理解 HTN 有幫助我才先開始看 STRIPS 並且順便寫一篇筆記的。

Wei Ji

前情提要

最近其實處於多開戰線的狀態:

Homelab 遷移大致完成,目前只剩下一個服務,想說該回來灌溉一下 RAG 專案了。

TiddlyRAG

關於目前整個生態系的亂象,我在之前的文章已經全部噴過一遍了,接下來就是採取一些行動了,關於這個專案的細節請見:

TiddlyRAG 計畫

細節我就不贅述,簡單來說就是我發現 TiddlyWiki 本身自帶 Chunk 、 Graph 、人類可讀可編輯和可攜性...等特性,在 LLM 時代應該是一個很好用的軟體,這也不是什麼新點子,想法去年 (2025) 十月就有了:

一種人類友善 llms.txt 構想

info

上面很多 Side Project 寫了一堆 TiddlyWiki 其實和 TiddlyRAG 是相關的,除了實驗一些 DDD 流程以外,也算是準備一些資料方便日後做 RAG 的測試。

嵌入伺服器與選擇

TiddlyRAG 的 POC 架構如下:

可以說有三個大題目:

  • 向量資料庫 (Vector database)
  • MCP (Model Context Protocol)
  • 資料嵌入 (Embedding)

向量資料庫之前工作寫小工具有接觸過了,MCP 的部份也稍微查過資料,剩下比較陌生的是資料嵌入的部份。

開放權重、OpenAI API 兼容、OCI (Open Container Initiative) 佈署、nvidia 解偶,這幾個算是我對於嵌入方案的基本要求。

首先是大名鼎鼎的 Ollama,不過很快我就發現幾個問題:

  • 它的映像檔過於肥大,單層超過 1GB 的映像檔在我的可憐網路下是拉不下來的。
  • 它只支援 nvidia 的 CUDA 和 AMD 的 ROCm,不夠通用。
  • 就算只使用 CPU 模式,映像檔是跟 nvidia 函式庫綁定的。

好吧,下一個看看以「性能優雅」出名的 llama.cpp。

斯巴拉西!

映像檔案小、支援多種 GPU 後端,看來就決定是你了!

llama.cpp

下載了 Ubuntu x64 (Vulkan)

./llama-server \
--hf-repo Qwen/Qwen3-Embedding-8B-GGUF \
--hf-file Qwen3-Embedding-8B-Q6_K.gguf \
--embeddings --pooling mean \
-c 4096 -ub 4096 -ngl 999 --no-mmap -fa on --no-webui

指令是參考別人的1,模型會下載到 ~/.cache/llama.cpp/,不知道能不能改路徑。

接著用 Typescript 戳一下:

const url = "http://localhost:8080/v1/embeddings"
const headers = {
"Authorization": `Bearer ANY`,
"Content-Type": "application/json"
}
const payload = {
"model": "ANY",
"input": "Your text string goes here",
"encoding_format": "float"
}

const response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(payload),
});

const result = await response.json()
console.log(JSON.stringify(result, undefined, 2));

GPU 有在運作,不錯不錯。

模型下載的一些問題

本來想說 llama.cpp 是精簡出名的,根據職責分離原則它可能不會實做下載的部份,所以先試著用 Huggingface 的 CLI 跑跑看了,

hf download \
--local-dir models \
Qwen/Qwen3-Embedding-8B-GGUF \
Qwen3-Embedding-8B-Q6_K.gguf \

不過還蠻不穩定的,不知道為什麼。

也有試過另外一個 llama.cpp 的指令:

./llama-server \
-hf Qwen/Qwen3-Embedding-8B-GGUF:Q6_K \
--embeddings --pooling mean \
-c 4096 -ub 4096 -ngl 999 --no-mmap -fa on --no-webui
load_backend: loaded RPC backend from /home/not-important/llama-b8267/libggml-rpc.so
ggml_vulkan: Found 1 Vulkan devices:
ggml_vulkan: 0 = Intel(R) Iris(R) Xe Graphics (RPL-P) (Intel open-source Mesa driver) | uma: 1 | fp16: 1 | bf16: 0 | warp size: 32 | shared memory: 65536 | int dot: 1 | matrix cores: none
load_backend: loaded Vulkan backend from /home/not-important/llama-b8267/libggml-vulkan.so
load_backend: loaded CPU backend from /home/not-important/llama-b8267/libggml-cpu-alderlake.so
common_download_file_single_online: no previous model file found /home/not-important/.cache/llama.cpp/Qwen_Qwen3-Embedding-8B-GGUF_preset.ini
common_download_file_single_online: HEAD failed, status: 404
no remote preset found, skipping
error from HF API (http://huggingface.mirrors.solid.arachne/v2/Qwen/Qwen3-Embedding-8B-GGUF/manifests/Q6_K), response code: 404, data: {"error":"Sorry, we can't find the page you are looking for."}

大概是因為 Olah 不支援 v2 API 吧。

info

我在 LAN 的模型快取策略是由 Homelab 處理,HF 原本用本地資料夾的「快取」我並不在乎。

GGUF

在上述範例中,我們看到了 Qwen3-Embedding-8B-Q6_K.gguf 這樣的檔案,它是什麼意思?

Qwen3-Embedding-8B 自然是模型本身的名稱以及它的參數量,.gguf 則是一種能夠儲存模型權重的檔案,並且 GGUF 是 GGML 的後繼者。

GGML 這個格式則是從 GGML (Georgi Gerganov Machine Learning Tensor library) 這個函式庫而來的,Georgi Gerganov 則是作者的名字。

GGUF 比較正確的全名其實是 "GGML Unified Format",關於它的名稱這裡有一篇文章在討論它:

What does GGUF stand for? A "Guide" : r/LocalLLaMA

Q6_K 則是量化參數,量化是一種用更小的資料型態來儲存模型權重的技術,可以減少模型儲存的大小與推論時需要的記憶體數量。Q6 代表模型主要使用 6bit 的資料來儲存權重,後面的數字或編號代表各種不同的混合量化策略,具體差異對於調用者而言不是特別重要,重要的是量化同時也會造成模型性能下降:

Perplexity 是一種相對指標,簡單來說準備一個資料集(例如維基百科),給定一些文字,讓模型預測下一個詞,並紀錄不正確性。我們可以看到 Q2_K 量化的 65B 模型退化到接近 30B 未量化模型的水準。

圖表來自 llama.cpp 的 GitHub,它使用了 Q2_K, Q3_K_S, Q3_K_M, Q3_K_L, Q4_K_S, Q4_K_M, Q5_K_S, Q5_K_M, Q6_K 這幾種量化參數並與 F16 進行比較,完整報告請見:

k-quants by ikawrakow · Pull Request #1684 · ggml-org/llama.cpp

這就是為什麼我選擇 Q6_K 量化的嵌入模型測試,因為它顯著的降低記憶體但是性能可能不會太顯著的下降。

info

Perplexity 是相對指標,Perplexity 指標沒有顯著下降不代表模型的其他性能沒有明顯下降。

Footnotes

  1. embedding with llama.cpp server : r/LocalLLaMA. https://www.reddit.com/r/LocalLLaMA/comments/1nqyi1x/comment/ngacugv/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button

Wei Ji

前情提要

Zero123++ 是第一個我想要自己跑得模型,它是一個「多視圖重建」模型,通常作為 「2D image to 3D model」的前置處理。

我對它有興趣有幾個原因,首先我對生成式 3D 模型有不小的興趣,對於獨立開發而言,經常使用基於 2D 的呈現方式,往往是受限於 3D 建模的成本高昂所致。

比起單純的文字提示詞生成,二維圖像生成三維模型具有有更高的可控性。在類神經模型領域,比起「one shot」我更喜歡將一個大問題切成數個小問題的方式,每個小問題都是單純的資料轉換過程,讓整體算法具有更高的透明度,以及更為可控的行為。並且正如我提到的,Zero123++ 是整個 「2D image to 3D model」 資料流 (pipeline) 的一個關鍵環節。

從高維度的資料萃取特徵,或是轉換成其他低微度資料,是一個不那麼有趣的過程。反之,「多視圖重建」本質是一個提高資料維度任務,因此是一個不適定問題 (ill-posed problem),不知道為什麼我對這類問題反而比較興奮(?)

至於為什麼要「自己跑」呢?因為這種需求算不上特別普遍,不像 LLM 已經有不少雲端供應商可以拿 API 直接打,這種野雞模型幾乎只能自己想辦法。

BUT,要運行 Zero123++ 需要大約 5GB 的 vRAM,我手上支援 CUDA 的外接 GPU 只有 4GB,這也是為什麼它是我「想自己跑」而不是「自己跑過」的模型...

RAM 就是 RAM

最近突然心想:

vRAM 來 vRAM 去的,啊不然我的內顯也是顯卡唄,它有多少 vRAM?

於是我試著下了個指令:

$ glxinfo | grep -E -i 'device|memory'
Device: Mesa Intel(R) Iris(R) Xe Graphics (RPL-P) (0xa7a0)
Video memory: 23941MB
Unified memory: yes
GL_AMD_performance_monitor, GL_AMD_pinned_memory,
GL_EXT_framebuffer_object, GL_EXT_framebuffer_sRGB, GL_EXT_memory_object,
GL_EXT_memory_object_fd, GL_EXT_packed_depth_stencil, GL_EXT_packed_float,
GL_AMD_pinned_memory, GL_AMD_query_buffer_object,
GL_EXT_gpu_program_parameters, GL_EXT_gpu_shader4, GL_EXT_memory_object,
GL_EXT_memory_object_fd, GL_EXT_multi_draw_arrays,
GL_EXT_instanced_arrays, GL_EXT_map_buffer_range, GL_EXT_memory_object,
GL_EXT_memory_object_fd, GL_EXT_multi_draw_arrays,

恩?我什麼時候 vRAM 自由了?

學習了一下才知道 GPU 和 RAM 之間有著頻寬限制,於是 Nvidia 乾脆把 RAM 塞進 GPU 內變成 vRAM,高度整合帶來的優勢就是速度提昇。

但是我不管啊,我就窮啊,要我等可以等啊,明明就有 RAM 卻不給我跑是怎樣?

ZLUDA

當世界壟罩在 Nvidia 的 CUDA 的淫威(?)之下,我越想越覺得這個走向不太對勁,為什麼我想要跑個軟體需要被特定的硬體綁架?vRAM 不夠,請買更多?為什麼用低階設備跑的妥協不存在?

於是最近我其實默默的在蒐集「反 CUDA 對策」口袋清單:

VUDA 使用的是替換標頭檔的方式處理:

#if defined(__NVCC__)
#include <cuda_runtime.h>
#else
#include <vuda_runtime.hpp>
#endif

但是這在已經編譯、打包好的 Python 環境顯得不太有效。

反觀,ZLUDA 的處理方式就簡單的多:

LD_LIBRARY_PATH="<ZLUDA_DIRECTORY>:$LD_LIBRARY_PATH" <APPLICATION> <APPLICATION_ARGUMENTS>

也就是直接用實做了 CUDA 界面的動態函式庫直接替換真正的 CUDA。

不過實際試過之後發現不能如預期的運行,調查了一下發現雖然 ZLUDA 自稱「CUDA on non-NVIDIA GPUs」,但是它的首要支援 (First class) 是 Windows + AMD。至於 Intel 嘛...翻來翻去才找到我要的解釋:

Firstly, if you simply want those frameworks to run on an Intel GPUs, then ZLUDA might not be the best way. Deep Learning is quite high on the Intel priority list. Every major framework (certainly PyTorch, Tensorflow, MXNet, PaddlePaddle) has a dedicated team working on it to bring the best performance on CPU and GPU. ... cuDNN API is really massive, it is a lot of work1

這才知道 Intel 本身就有在這方面做一些努力,並且工作量也十分龐大,因此 ZLUDA 專案並不優先處理這方面 (Intel) 的需求。

加上 ZLUDA 專案本身跟 AMD 就走得比較近,底層實做也高度仰賴 ROCm 2

Intel Extension for PyTorch

  • Intel Extension for PyTorch
    • 2k ⭐

有一些教學會說要安裝 oneAPI,一開始我也照做了,但是問題依然無法排除,直到我看到:

When using torch-xpu from the PyTorch channel, you do not need to manually source your oneAPI environment. Instead, you can install it directly as follows:

所以關於 oneAPI 的東西我這裡就不提了。

而 Intel 官方的安裝指引則是如下3

python -m pip install torch==2.8.0 torchvision==0.23.0 torchaudio==2.8.0 --index-url https://download.pytorch.org/whl/xpu
python -m pip install intel-extension-for-pytorch==2.8.10+xpu --extra-index-url https://pytorch-extension.intel.com/release-whl/stable/xpu/us/
python -m pip install oneccl_bind_pt==2.8.0+xpu --index-url https://pytorch-extension.intel.com/release-whl/stable/xpu/us/

翻譯成 PDM 大概長這樣:

# pyproject.toml
dependencies = [
"torch==2.8.0",
"torchvision==0.23.0",
"torchaudio==2.8.0",
"intel-extension-for-pytorch==2.8.10+xpu",
"oneccl_bind_pt==2.8.0+xpu",
]
[tool.pdm.scripts]
start.cmd = "python examples/img_to_mv.py"
start.env = { OverrideDefaultFP64Settings = "1", IGC_EnableDPEmulation = "1" }

[[tool.pdm.source]]
name = "pytorch"
url = "https://download.pytorch.org/whl/xpu"
include_packages = ["torch", "torchvision", "torchaudio", "pytorch-triton-xpu"]
exclude_packages = ["*"]

[[tool.pdm.source]]
name = "intel"
url = "https://pytorch-extension.intel.com/release-whl/stable/xpu/us/"
include_packages = ["intel-extension-for-pytorch", "oneccl_bind_pt"]
exclude_packages = ["*"]

這兩個環境變數則是用來修復 UR error 的問題4

set OverrideDefaultFP64Settings=1
set IGC_EnableDPEmulation=1

最後成功花了 15 分鐘把這個:

變成這個:

Footnotes

  1. Any plans for testing this with Pytorch/Tensorflow/mxnet? · Issue #17 · vosen/ZLUDA. https://github.com/vosen/ZLUDA/issues/17

  2. 讓 IntelAMD GPU 直接跑 CUDA 程式的 ZLUDA – Gea-Suan Lin's BLOG. Retrieved 2026-01-22, from https://blog.gslin.org/archives/2024/02/13/11655/%E8%AE%93-intelamd-gpu-%E7%9B%B4%E6%8E%A5%E8%B7%91-cuda-%E7%A8%8B%E5%BC%8F%E7%9A%84-zluda/

  3. Intel® Extension for PyTorch* Installation Guide. Retrieved 2026-01-22, from https://pytorch-extension.intel.com/installation

  4. RuntimeError: UR error on ADL-N with IPEX 2.5/2.6 using .to(torch.float16) · Issue #800 · intel/intel-extension-for-pytorch. https://github.com/intel/intel-extension-for-pytorch/issues/800#issuecomment-3166243912