mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-07-29 07:45:55 +02:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f87067841b | |||
| c6292cfb8e | |||
| 91f8c9c5fb | |||
| 1cbfd19883 | |||
| 0e4a036223 | |||
| b77d646751 | |||
| 0324696b8e | |||
| 8e8681e0e2 | |||
| dee2a846b8 | |||
| 7ef790f90a | |||
| ddfc2288e4 | |||
| 419b881c02 | |||
| b910200897 | |||
| ad256ded30 | |||
| d73c1d6b22 | |||
| 88b47a755c | |||
| 3d1c3a8975 | |||
| 0d47ea7427 | |||
| d4d057b6dd | |||
| 7657a6c26a | |||
| 55b7d6c4c7 | |||
| d2a818231e | |||
| af285020e9 | |||
| b1d4c65524 | |||
| 42fc243060 | |||
| ff067f76dd | |||
| 7cdd557f76 | |||
| 8bb909374d |
@@ -0,0 +1,90 @@
|
||||
name: CI (wasm)
|
||||
|
||||
on:
|
||||
workflow_dispatch: # allows manual triggering
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths: [
|
||||
'.github/workflows/build-wasm.yml',
|
||||
'**/CMakeLists.txt',
|
||||
'**/.cmake',
|
||||
'**/*.h',
|
||||
'**/*.hpp',
|
||||
'**/*.c',
|
||||
'**/*.cpp',
|
||||
'**/*.wgsl',
|
||||
'**/*.tmpl',
|
||||
'ggml/src/ggml-webgpu/wgsl-shaders/embed_wgsl.py'
|
||||
]
|
||||
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths: [
|
||||
'.github/workflows/build-wasm.yml',
|
||||
'**/CMakeLists.txt',
|
||||
'**/.cmake',
|
||||
'**/*.h',
|
||||
'**/*.hpp',
|
||||
'**/*.c',
|
||||
'**/*.cpp',
|
||||
'**/*.wgsl',
|
||||
'**/*.tmpl',
|
||||
'ggml/src/ggml-webgpu/wgsl-shaders/embed_wgsl.py'
|
||||
]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
GGML_NLOOP: 3
|
||||
GGML_N_THREADS: 1
|
||||
LLAMA_ARG_LOG_COLORS: 1
|
||||
LLAMA_ARG_LOG_PREFIX: 1
|
||||
LLAMA_ARG_LOG_TIMESTAMPS: 1
|
||||
|
||||
jobs:
|
||||
ubuntu-webgpu:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: ccache
|
||||
uses: ggml-org/ccache-action@v1.2.21
|
||||
with:
|
||||
key: webgpu-ubuntu-24.04-arm-wasm
|
||||
evict-old-files: 1d
|
||||
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||
|
||||
- name: Install Emscripten
|
||||
run: |
|
||||
git clone https://github.com/emscripten-core/emsdk.git
|
||||
cd emsdk
|
||||
./emsdk install latest
|
||||
./emsdk activate latest
|
||||
|
||||
- name: Fetch emdawnwebgpu
|
||||
run: |
|
||||
DAWN_TAG="v20260317.182325"
|
||||
EMDAWN_PKG="emdawnwebgpu_pkg-${DAWN_TAG}.zip"
|
||||
echo "Downloading ${EMDAWN_PKG}"
|
||||
curl -L -o emdawn.zip \
|
||||
"https://github.com/google/dawn/releases/download/${DAWN_TAG}/${EMDAWN_PKG}"
|
||||
unzip emdawn.zip
|
||||
|
||||
- name: Build WASM WebGPU
|
||||
run: |
|
||||
source emsdk/emsdk_env.sh
|
||||
emcmake cmake -B build-wasm \
|
||||
-G "Ninja" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DGGML_WEBGPU=ON \
|
||||
-DGGML_OPENMP=OFF \
|
||||
-DLLAMA_OPENSSL=OFF \
|
||||
-DEMDAWNWEBGPU_DIR=emdawnwebgpu_pkg
|
||||
|
||||
time cmake --build build-wasm --config Release --target test-backend-ops -j $(nproc)
|
||||
@@ -13,7 +13,9 @@ on:
|
||||
'**/*.hpp',
|
||||
'**/*.c',
|
||||
'**/*.cpp',
|
||||
'**/*.wgsl'
|
||||
'**/*.wgsl',
|
||||
'**/*.tmpl',
|
||||
'ggml/src/ggml-webgpu/wgsl-shaders/embed_wgsl.py'
|
||||
]
|
||||
|
||||
pull_request:
|
||||
@@ -151,46 +153,3 @@ jobs:
|
||||
# This is using llvmpipe and runs slower than other backends
|
||||
# test-backend-ops is too slow on llvmpipe, skip it
|
||||
ctest -L main -E test-backend-ops --verbose --timeout 900
|
||||
|
||||
ubuntu-wasm:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: ccache
|
||||
uses: ggml-org/ccache-action@v1.2.21
|
||||
with:
|
||||
key: webgpu-ubuntu-24.04-arm-wasm
|
||||
evict-old-files: 1d
|
||||
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||
|
||||
- name: Install Emscripten
|
||||
run: |
|
||||
git clone https://github.com/emscripten-core/emsdk.git
|
||||
cd emsdk
|
||||
./emsdk install latest
|
||||
./emsdk activate latest
|
||||
|
||||
- name: Fetch emdawnwebgpu
|
||||
run: |
|
||||
DAWN_TAG="v20260317.182325"
|
||||
EMDAWN_PKG="emdawnwebgpu_pkg-${DAWN_TAG}.zip"
|
||||
echo "Downloading ${EMDAWN_PKG}"
|
||||
curl -L -o emdawn.zip \
|
||||
"https://github.com/google/dawn/releases/download/${DAWN_TAG}/${EMDAWN_PKG}"
|
||||
unzip emdawn.zip
|
||||
|
||||
- name: Build WASM WebGPU
|
||||
run: |
|
||||
source emsdk/emsdk_env.sh
|
||||
emcmake cmake -B build-wasm \
|
||||
-G "Ninja" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DGGML_WEBGPU=ON \
|
||||
-DLLAMA_OPENSSL=OFF \
|
||||
-DEMDAWNWEBGPU_DIR=emdawnwebgpu_pkg
|
||||
|
||||
time cmake --build build-wasm --config Release --target test-backend-ops -j $(nproc)
|
||||
|
||||
@@ -84,6 +84,14 @@ else()
|
||||
set(LLAMA_TOOLS_INSTALL_DEFAULT ${LLAMA_STANDALONE})
|
||||
endif()
|
||||
|
||||
# subprocess spawning isn't a supported/sandbox-friendly operation on mobile OSes or in WASM
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "iOS" OR CMAKE_SYSTEM_NAME STREQUAL "Android" OR ANDROID
|
||||
OR CMAKE_SYSTEM_NAME STREQUAL "Emscripten" OR EMSCRIPTEN)
|
||||
set(LLAMA_SUBPROCESS_DEFAULT OFF)
|
||||
else()
|
||||
set(LLAMA_SUBPROCESS_DEFAULT ON)
|
||||
endif()
|
||||
|
||||
#
|
||||
# option list
|
||||
#
|
||||
@@ -117,6 +125,7 @@ option(LLAMA_TESTS_INSTALL "llama: install tests" ON)
|
||||
|
||||
# 3rd party libs
|
||||
option(LLAMA_OPENSSL "llama: use openssl to support HTTPS" ON)
|
||||
option(LLAMA_SUBPROCESS "llama-common: use subprocess, required by server tools and server router mode" ${LLAMA_SUBPROCESS_DEFAULT})
|
||||
option(LLAMA_LLGUIDANCE "llama-common: include LLGuidance library for structured output in common utils" OFF)
|
||||
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ For more info, please refer to the [AGENTS.md](AGENTS.md) file.
|
||||
- When merging a PR, make sure you have a good understanding of the changes
|
||||
- If a PR does not warrant a new release, add `[no release]` in the squashed commit to spare CI resources
|
||||
- Be mindful of maintenance: most of the work going into a feature happens after the PR is merged. If the PR author is not committed to contribute long-term, someone else needs to take responsibility (you)
|
||||
- Add the ["merge ready"](https://github.com/ggml-org/llama.cpp/pulls?q=is%3Apr+is%3Aopen+draft%3Ano+sort%3Aupdated-desc+label%3A%22merge+ready%22+) label to a PR to indicate when a PR can be fast-merged without waiting for 2 independent reviews. [(more info)](https://github.com/ggml-org/llama.cpp/pull/26178)
|
||||
|
||||
Maintainers reserve the right to decline review or close pull requests for any reason, without any questions, particularly under any of the following conditions:
|
||||
- The proposed change is already mentioned in the roadmap or an existing issue, and it has been assigned to someone.
|
||||
|
||||
@@ -100,6 +100,8 @@ add_library(${TARGET}
|
||||
sampling.h
|
||||
speculative.cpp
|
||||
speculative.h
|
||||
subproc.cpp
|
||||
subproc.h
|
||||
trie.cpp
|
||||
trie.h
|
||||
unicode.cpp
|
||||
@@ -127,6 +129,10 @@ set_target_properties(${TARGET} PROPERTIES
|
||||
target_include_directories(${TARGET} PUBLIC . ../vendor)
|
||||
target_compile_features (${TARGET} PUBLIC cxx_std_17)
|
||||
|
||||
if (LLAMA_SUBPROCESS)
|
||||
target_compile_definitions(${TARGET} PUBLIC LLAMA_SUBPROCESS)
|
||||
endif()
|
||||
|
||||
if (BUILD_SHARED_LIBS)
|
||||
set_target_properties(${TARGET} PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
|
||||
+46
-20
@@ -539,6 +539,13 @@ void common_models_handler_apply(common_models_handler & handler, common_params
|
||||
}
|
||||
};
|
||||
|
||||
// an explicit draft file selection (e.g. -md with -hfd) disables the sidecar resolution of the draft repo
|
||||
if (!params.speculative.draft.mparams.hf_file.empty()) {
|
||||
plan_spec.mtp = {};
|
||||
plan_spec.dflash = {};
|
||||
plan_spec.eagle3 = {};
|
||||
}
|
||||
|
||||
// infer the speculative type from the sidecar shipped by the draft repo when none is requested
|
||||
if (spec_types_is_default(params)) {
|
||||
if (!plan_spec.mtp.local_path.empty()) {
|
||||
@@ -588,6 +595,11 @@ void common_models_handler_apply(common_models_handler & handler, common_params
|
||||
});
|
||||
}
|
||||
|
||||
// a wired draft sidecar counts as an explicit draft for the main plan fallback below
|
||||
if (spec_sidecar_found) {
|
||||
had_spec_url = true;
|
||||
}
|
||||
|
||||
// handle plan_spec (e.g. --spec-draft-hf)
|
||||
if (!plan_spec.model_files.empty() && !had_spec_url && !spec_sidecar_found) {
|
||||
add_tasks(plan_spec.model_files, plan_spec.primary, params.speculative.draft.mparams);
|
||||
@@ -1049,6 +1061,31 @@ static std::vector<ggml_backend_dev_t> parse_device_list(const std::string & val
|
||||
return devices;
|
||||
}
|
||||
|
||||
void common_print_available_devices() {
|
||||
constexpr size_t MiB = 1024 * 1024;
|
||||
std::vector<ggml_backend_dev_t> devices;
|
||||
|
||||
ggml_backend_load_all();
|
||||
|
||||
for (size_t i = 0; i < ggml_backend_dev_count(); ++i) {
|
||||
auto * dev = ggml_backend_dev_get(i);
|
||||
if (ggml_backend_dev_type(dev) != GGML_BACKEND_DEVICE_TYPE_CPU) {
|
||||
devices.push_back(dev);
|
||||
}
|
||||
}
|
||||
printf("Available devices:\n");
|
||||
|
||||
if (devices.empty()) {
|
||||
printf(" (none)\n");
|
||||
return;
|
||||
}
|
||||
for (auto * dev : devices) {
|
||||
size_t free, total;
|
||||
ggml_backend_dev_memory(dev, &free, &total);
|
||||
printf(" %s: %s (%zu MiB, %zu MiB free)\n", ggml_backend_dev_name(dev), ggml_backend_dev_description(dev), total / MiB, free / MiB);
|
||||
}
|
||||
}
|
||||
|
||||
static void add_rpc_devices(const std::string & servers) {
|
||||
auto rpc_servers = string_split<std::string>(servers, ',');
|
||||
if (rpc_servers.empty()) {
|
||||
@@ -2508,7 +2545,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
|
||||
}
|
||||
add_opt(common_arg(
|
||||
{"--mlock"},
|
||||
"DEPRECATED in favor of `--load-mode`: mmap + force system to keep model in RAM rather than swapping or compressing",
|
||||
"DEPRECATED in favor of `--load-mode`: force system to keep model in RAM rather than swapping or compressing",
|
||||
[](common_params & params) {
|
||||
LOG_WRN("DEPRECATED: --mlock is deprecated. use --load-mode mlock instead\n");
|
||||
params.load_mode = LLAMA_LOAD_MODE_MLOCK;
|
||||
@@ -2537,13 +2574,15 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
|
||||
"model loading mode (default: mmap)\n"
|
||||
"- none: no special loading mode\n"
|
||||
"- mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)\n"
|
||||
"- mlock: mmap + force system to keep model in RAM rather than swapping or compressing\n"
|
||||
"- mlock: force system to keep model in RAM rather than swapping or compressing\n"
|
||||
"- mmap+mlock: mmap + force system to keep model in RAM rather than swapping or compressing\n"
|
||||
"- dio: use DirectIO if available\n",
|
||||
[](common_params & params, const std::string & value) {
|
||||
/**/ if (value == "none") { params.load_mode = LLAMA_LOAD_MODE_NONE; }
|
||||
else if (value == "mmap") { params.load_mode = LLAMA_LOAD_MODE_MMAP; }
|
||||
else if (value == "mlock") { params.load_mode = LLAMA_LOAD_MODE_MLOCK; }
|
||||
else if (value == "dio") { params.load_mode = LLAMA_LOAD_MODE_DIRECT_IO; }
|
||||
/**/ if (value == "none") { params.load_mode = LLAMA_LOAD_MODE_NONE; }
|
||||
else if (value == "mmap") { params.load_mode = LLAMA_LOAD_MODE_MMAP; }
|
||||
else if (value == "mlock") { params.load_mode = LLAMA_LOAD_MODE_MLOCK; }
|
||||
else if (value == "mmap+mlock") { params.load_mode = LLAMA_LOAD_MODE_MMAP_MLOCK; }
|
||||
else if (value == "dio") { params.load_mode = LLAMA_LOAD_MODE_DIRECT_IO; }
|
||||
else { throw std::invalid_argument("invalid value"); }
|
||||
}
|
||||
).set_env("LLAMA_ARG_LOAD_MODE"));
|
||||
@@ -2574,20 +2613,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
|
||||
{"--list-devices"},
|
||||
"print list of available devices and exit",
|
||||
[](common_params &) {
|
||||
ggml_backend_load_all();
|
||||
std::vector<ggml_backend_dev_t> devices;
|
||||
for (size_t i = 0; i < ggml_backend_dev_count(); ++i) {
|
||||
auto * dev = ggml_backend_dev_get(i);
|
||||
if (ggml_backend_dev_type(dev) != GGML_BACKEND_DEVICE_TYPE_CPU) {
|
||||
devices.push_back(dev);
|
||||
}
|
||||
}
|
||||
printf("Available devices:\n");
|
||||
for (auto * dev : devices) {
|
||||
size_t free, total;
|
||||
ggml_backend_dev_memory(dev, &free, &total);
|
||||
printf(" %s: %s (%zu MiB, %zu MiB free)\n", ggml_backend_dev_name(dev), ggml_backend_dev_description(dev), total / 1024 / 1024, free / 1024 / 1024);
|
||||
}
|
||||
common_print_available_devices();
|
||||
exit(0);
|
||||
}
|
||||
));
|
||||
|
||||
@@ -123,6 +123,9 @@ struct common_params_context {
|
||||
// if one argument has invalid value, it will automatically display usage of the specific argument (and not the full usage message)
|
||||
bool common_params_parse(int argc, char ** argv, common_params & params, llama_example ex, void(*print_usage)(int, char **) = nullptr);
|
||||
|
||||
// load all backends and print the list of available (non-CPU) devices to stdout
|
||||
void common_print_available_devices();
|
||||
|
||||
// parse input arguments from CLI into a map
|
||||
bool common_params_to_map(int argc, char ** argv, llama_example ex, std::map<common_arg, std::string> & out_map);
|
||||
|
||||
|
||||
@@ -1249,7 +1249,6 @@ common_init_result::common_init_result(common_params & params, bool model_only)
|
||||
lora.reset(llama_adapter_lora_init(model, la.path.c_str()));
|
||||
if (lora == nullptr) {
|
||||
COM_ERR("failed to load lora adapter '%s'\n", la.path.c_str());
|
||||
pimpl->model.reset(model);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+52
-17
@@ -568,16 +568,30 @@ static hf_cache::hf_files get_split_files(const hf_cache::hf_files & files,
|
||||
}
|
||||
|
||||
// pick the best sibling GGUF whose filename contains `keyword` (e.g. "mmproj" / "mtp"),
|
||||
// preferring deeper shared directory prefix with the model, then closest quantization
|
||||
// preferring deeper shared directory prefix with the model, then exact `tag` match,
|
||||
// then closest quantization to the tag when given, or to the model otherwise
|
||||
static hf_cache::hf_file find_best_sibling(const hf_cache::hf_files & files,
|
||||
const std::string & model,
|
||||
const std::string & keyword) {
|
||||
const std::string & keyword,
|
||||
const std::string & tag = "") {
|
||||
hf_cache::hf_file best;
|
||||
size_t best_depth = 0;
|
||||
int best_diff = 0;
|
||||
bool best_exact = false;
|
||||
bool found = false;
|
||||
|
||||
auto model_bits = extract_quant_bits(model);
|
||||
std::string tag_upper = tag;
|
||||
for (char & c : tag_upper) {
|
||||
c = (char) std::toupper((unsigned char) c);
|
||||
}
|
||||
|
||||
int model_bits = 0;
|
||||
if (!tag_upper.empty()) {
|
||||
auto pos = tag_upper.find_first_of("0123456789");
|
||||
model_bits = pos == std::string::npos ? 0 : std::stoi(tag_upper.substr(pos));
|
||||
} else {
|
||||
model_bits = extract_quant_bits(model);
|
||||
}
|
||||
auto model_parts = string_split<std::string>(model, '/');
|
||||
auto model_dir = model_parts.end() - 1;
|
||||
|
||||
@@ -600,10 +614,19 @@ static hf_cache::hf_file find_best_sibling(const hf_cache::hf_files & files,
|
||||
auto bits = extract_quant_bits(f.path);
|
||||
auto diff = std::abs(bits - model_bits);
|
||||
|
||||
if (!found || depth > best_depth || (depth == best_depth && diff < best_diff)) {
|
||||
std::string path_upper = f.path;
|
||||
for (char & c : path_upper) {
|
||||
c = (char) std::toupper((unsigned char) c);
|
||||
}
|
||||
bool exact = !tag_upper.empty() && path_upper.find("-" + tag_upper + ".") != std::string::npos;
|
||||
|
||||
if (!found || depth > best_depth ||
|
||||
(depth == best_depth && exact && !best_exact) ||
|
||||
(depth == best_depth && exact == best_exact && diff < best_diff)) {
|
||||
best = f;
|
||||
best_depth = depth;
|
||||
best_diff = diff;
|
||||
best_exact = exact;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
@@ -616,18 +639,21 @@ static hf_cache::hf_file find_best_mmproj(const hf_cache::hf_files & files,
|
||||
}
|
||||
|
||||
static hf_cache::hf_file find_best_mtp(const hf_cache::hf_files & files,
|
||||
const std::string & model) {
|
||||
return find_best_sibling(files, model, "mtp-");
|
||||
const std::string & model,
|
||||
const std::string & tag = "") {
|
||||
return find_best_sibling(files, model, "mtp-", tag);
|
||||
}
|
||||
|
||||
static hf_cache::hf_file find_best_eagle3(const hf_cache::hf_files & files,
|
||||
const std::string & model) {
|
||||
return find_best_sibling(files, model, "eagle3-");
|
||||
const std::string & model,
|
||||
const std::string & tag = "") {
|
||||
return find_best_sibling(files, model, "eagle3-", tag);
|
||||
}
|
||||
|
||||
static hf_cache::hf_file find_best_dflash(const hf_cache::hf_files & files,
|
||||
const std::string & model) {
|
||||
return find_best_sibling(files, model, "dflash-");
|
||||
const std::string & model,
|
||||
const std::string & tag = "") {
|
||||
return find_best_sibling(files, model, "dflash-", tag);
|
||||
}
|
||||
|
||||
static bool gguf_filename_is_model(const std::string & filepath) {
|
||||
@@ -736,27 +762,36 @@ common_download_hf_plan common_download_get_hf_plan(const common_params_model &
|
||||
}
|
||||
} else {
|
||||
primary = find_best_model(all, tag);
|
||||
if (primary.path.empty()) {
|
||||
// a requested sidecar can resolve on its own, without a full model of the same tag
|
||||
if (primary.path.empty() && !opts.download_mtp && !opts.download_dflash && !opts.download_eagle3) {
|
||||
LOG_ERR("%s: no GGUF files found in repository %s\n", __func__, repo.c_str());
|
||||
list_available_gguf_files(all);
|
||||
return plan;
|
||||
}
|
||||
}
|
||||
|
||||
plan.primary = primary;
|
||||
plan.model_files = get_split_files(all, primary);
|
||||
if (!primary.path.empty()) {
|
||||
plan.primary = primary;
|
||||
plan.model_files = get_split_files(all, primary);
|
||||
}
|
||||
|
||||
if (opts.download_mmproj) {
|
||||
if (opts.download_mmproj && !primary.path.empty()) {
|
||||
plan.mmproj = find_best_mmproj(all, primary.path);
|
||||
}
|
||||
if (opts.download_mtp) {
|
||||
plan.mtp = find_best_mtp(all, primary.path);
|
||||
plan.mtp = find_best_mtp(all, primary.path, tag);
|
||||
}
|
||||
if (opts.download_dflash) {
|
||||
plan.dflash = find_best_dflash(all, primary.path);
|
||||
plan.dflash = find_best_dflash(all, primary.path, tag);
|
||||
}
|
||||
if (opts.download_eagle3) {
|
||||
plan.eagle3 = find_best_eagle3(all, primary.path);
|
||||
plan.eagle3 = find_best_eagle3(all, primary.path, tag);
|
||||
}
|
||||
|
||||
if (primary.path.empty() &&
|
||||
plan.mtp.local_path.empty() && plan.dflash.local_path.empty() && plan.eagle3.local_path.empty()) {
|
||||
LOG_ERR("%s: no GGUF files found in repository %s\n", __func__, repo.c_str());
|
||||
list_available_gguf_files(all);
|
||||
}
|
||||
|
||||
return plan;
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ static std::vector<llama_device_memory_data> common_get_device_memory_data_impl(
|
||||
devs.push_back(llama_model_get_device(model, i));
|
||||
}
|
||||
|
||||
hp_ngl = llama_model_n_layer(model);
|
||||
hp_ngl = llama_model_n_layer(model) + llama_model_n_layer_nextn(model);
|
||||
hp_n_ctx_train = llama_model_n_ctx_train(model);
|
||||
hp_n_expert = llama_model_n_expert(model);
|
||||
|
||||
|
||||
+15
-5
@@ -437,6 +437,7 @@ struct common_speculative_impl_draft_eagle3 : public common_speculative_impl {
|
||||
int32_t n_embd_dec = 0; // draft hidden size
|
||||
int32_t n_embd_enc = 0; // target_layer_ids_n * target_hidden_size
|
||||
int32_t n_embd_tgt = 0; // target model hidden size
|
||||
int32_t n_layer_tgt = 0; // target model layer count
|
||||
|
||||
const int32_t * target_layer_ids = nullptr; // model_dft's extract layer indices
|
||||
uint32_t target_layer_ids_n = 0;
|
||||
@@ -478,6 +479,7 @@ struct common_speculative_impl_draft_eagle3 : public common_speculative_impl {
|
||||
n_embd_tgt = llama_model_n_embd(model_tgt);
|
||||
n_embd_dec = llama_model_n_embd(model_dft);
|
||||
n_embd_enc = (int32_t) target_layer_ids_n * n_embd_tgt;
|
||||
n_layer_tgt = llama_model_n_layer(model_tgt);
|
||||
|
||||
const int32_t n_b = (int32_t) llama_n_batch(ctx_dft);
|
||||
batch = llama_batch_init(/*n_tokens=*/ n_b, /*embd=*/ n_embd_dec, /*n_seq_max=*/ 1);
|
||||
@@ -510,9 +512,15 @@ struct common_speculative_impl_draft_eagle3 : public common_speculative_impl {
|
||||
}
|
||||
}
|
||||
|
||||
// turn on extraction of the target layers' input embeddings
|
||||
// turn on extraction of the target layers' hidden states
|
||||
for (uint32_t k = 0; k < target_layer_ids_n; ++k) {
|
||||
llama_set_embeddings_layer_inp(ctx_tgt, (uint32_t) target_layer_ids[k], true);
|
||||
if (target_layer_ids[k] < n_layer_tgt) {
|
||||
llama_set_embeddings_layer_inp(ctx_tgt, (uint32_t) target_layer_ids[k], true);
|
||||
} else if (target_layer_ids[k] == n_layer_tgt) {
|
||||
llama_set_embeddings_nextn(ctx_tgt, true, /*masked*/ false);
|
||||
} else {
|
||||
GGML_ABORT("EAGLE3: target layer id %d exceeds target n_layer %d", target_layer_ids[k], n_layer_tgt);
|
||||
}
|
||||
}
|
||||
|
||||
// turn on extraction of the draft model's pre-norm hidden state
|
||||
@@ -600,7 +608,9 @@ struct common_speculative_impl_draft_eagle3 : public common_speculative_impl {
|
||||
features_buf.resize((size_t) n_tokens * n_embd_enc, 0.0f);
|
||||
|
||||
for (uint32_t k = 0; k < target_layer_ids_n; ++k) {
|
||||
const float * layer = llama_get_embeddings_layer_inp(ctx_tgt, (uint32_t) target_layer_ids[k]);
|
||||
const float * layer = target_layer_ids[k] < n_layer_tgt
|
||||
? llama_get_embeddings_layer_inp(ctx_tgt, (uint32_t) target_layer_ids[k])
|
||||
: llama_get_embeddings_nextn(ctx_tgt);
|
||||
if (!layer) {
|
||||
GGML_ABORT("EAGLE3: target layer %d input not extracted.", target_layer_ids[k]);
|
||||
}
|
||||
@@ -2284,7 +2294,7 @@ common_speculative_init_result::common_speculative_init_result(
|
||||
std::string model_path;
|
||||
if (has_draft) {
|
||||
model_path = params.speculative.draft.mparams.path;
|
||||
LOG_TRC("%s: loading draft model '%s'\n", __func__, model_path.c_str());
|
||||
LOG_INF("%s: loading draft model '%s'\n", __func__, model_path.c_str());
|
||||
|
||||
llama_model * model_dft = llama_model_load_from_file(params.model.path.c_str(), mparams);
|
||||
if (model_dft == NULL) {
|
||||
@@ -2304,7 +2314,7 @@ common_speculative_init_result::common_speculative_init_result(
|
||||
} else if (spec_mtp) {
|
||||
model_path = params.model.path;
|
||||
|
||||
LOG_TRC("%s: creating MTP draft context against the target model '%s'\n", __func__, model_path.c_str());
|
||||
LOG_INF("%s: creating MTP draft context against the target model '%s'\n", __func__, model_path.c_str());
|
||||
|
||||
llama_context * ctx_dft = llama_init_from_model(model_tgt, cparams);
|
||||
if (ctx_dft == nullptr) {
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
#include "subproc.h"
|
||||
|
||||
bool common_subproc::is_supported() {
|
||||
#ifdef LLAMA_SUBPROCESS
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef LLAMA_SUBPROCESS
|
||||
|
||||
static std::vector<char *> to_cstr_vec(const std::vector<std::string> & v) {
|
||||
std::vector<char *> r;
|
||||
r.reserve(v.size() + 1);
|
||||
for (const auto & s : v) {
|
||||
r.push_back(const_cast<char *>(s.c_str()));
|
||||
}
|
||||
r.push_back(nullptr);
|
||||
return r;
|
||||
}
|
||||
|
||||
common_subproc::~common_subproc() {
|
||||
if (is_created) {
|
||||
subprocess_destroy(&proc);
|
||||
is_created = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool common_subproc::create(
|
||||
const std::vector<std::string> & args,
|
||||
int options,
|
||||
const std::vector<std::string> & env,
|
||||
const char * cwd) {
|
||||
auto argv = to_cstr_vec(args);
|
||||
|
||||
int result;
|
||||
if (env.empty() && cwd == nullptr) {
|
||||
result = subprocess_create(argv.data(), options, &proc);
|
||||
} else {
|
||||
auto envp = to_cstr_vec(env);
|
||||
result = subprocess_create_ex(argv.data(), options, env.empty() ? nullptr : envp.data(), cwd, &proc);
|
||||
}
|
||||
|
||||
is_created = result == 0;
|
||||
return is_created;
|
||||
}
|
||||
|
||||
bool common_subproc::has_handle() const {
|
||||
if (!is_created) {
|
||||
return false;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
return proc.hProcess != nullptr;
|
||||
#else
|
||||
return proc.child > 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool common_subproc::alive() {
|
||||
return is_created && subprocess_alive(&proc);
|
||||
}
|
||||
|
||||
FILE * common_subproc::stdin_file() {
|
||||
return is_created ? subprocess_stdin(&proc) : nullptr;
|
||||
}
|
||||
|
||||
FILE * common_subproc::stdout_file() {
|
||||
return is_created ? subprocess_stdout(&proc) : nullptr;
|
||||
}
|
||||
|
||||
FILE * common_subproc::stderr_file() {
|
||||
return is_created ? subprocess_stderr(&proc) : nullptr;
|
||||
}
|
||||
|
||||
void common_subproc::close_stdin() {
|
||||
if (is_created && proc.stdin_file) {
|
||||
fclose(proc.stdin_file);
|
||||
proc.stdin_file = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void common_subproc::terminate() {
|
||||
if (has_handle()) {
|
||||
subprocess_terminate(&proc);
|
||||
}
|
||||
}
|
||||
|
||||
int common_subproc::join() {
|
||||
int exit_code = -1;
|
||||
if (is_created) {
|
||||
subprocess_join(&proc, &exit_code);
|
||||
subprocess_destroy(&proc);
|
||||
is_created = false;
|
||||
}
|
||||
return exit_code;
|
||||
}
|
||||
|
||||
#else // !LLAMA_SUBPROCESS
|
||||
|
||||
common_subproc::~common_subproc() = default;
|
||||
|
||||
bool common_subproc::create(
|
||||
const std::vector<std::string> &,
|
||||
int,
|
||||
const std::vector<std::string> &,
|
||||
const char *) {
|
||||
(void)(proc);
|
||||
(void)(is_created);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool common_subproc::has_handle() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool common_subproc::alive() {
|
||||
return false;
|
||||
}
|
||||
|
||||
FILE * common_subproc::stdin_file() {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FILE * common_subproc::stdout_file() {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FILE * common_subproc::stderr_file() {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void common_subproc::close_stdin() {
|
||||
}
|
||||
|
||||
void common_subproc::terminate() {
|
||||
}
|
||||
|
||||
int common_subproc::join() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
#endif // LLAMA_SUBPROCESS
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifdef LLAMA_SUBPROCESS
|
||||
#include <sheredom/subprocess.h>
|
||||
#else
|
||||
// dummy values to allow compilation when subprocess is disabled
|
||||
struct subprocess_s {};
|
||||
static constexpr int subprocess_option_no_window = 0;
|
||||
static constexpr int subprocess_option_combined_stdout_stderr = 0;
|
||||
static constexpr int subprocess_option_inherit_environment = 0;
|
||||
static constexpr int subprocess_option_search_user_path = 0;
|
||||
#endif
|
||||
|
||||
// RAII-style wrapper around https://github.com/sheredom/subprocess.h,
|
||||
// exposing method calls instead of free functions operating on subprocess_s.
|
||||
struct common_subproc {
|
||||
common_subproc() = default;
|
||||
~common_subproc();
|
||||
|
||||
common_subproc(const common_subproc &) = delete;
|
||||
common_subproc & operator=(const common_subproc &) = delete;
|
||||
|
||||
// spawn a child process; if env is non-empty it replaces the child's environment
|
||||
// (do not combine with subprocess_option_inherit_environment)
|
||||
bool create(
|
||||
const std::vector<std::string> & args,
|
||||
int options,
|
||||
const std::vector<std::string> & env = {},
|
||||
const char * cwd = nullptr);
|
||||
|
||||
bool alive();
|
||||
|
||||
// true if LLAMA_SUBPROCESS was enabled at build time; when false, create() always fails
|
||||
static bool is_supported();
|
||||
|
||||
FILE * stdin_file();
|
||||
FILE * stdout_file();
|
||||
FILE * stderr_file();
|
||||
|
||||
// close stdin and detach it from the process, so a later join()/destroy() won't double-close it;
|
||||
// use this after writing all input to signal EOF to the child while it's still running
|
||||
void close_stdin();
|
||||
|
||||
void terminate();
|
||||
|
||||
// wait for the process to exit, release the underlying handle and return its exit code
|
||||
int join();
|
||||
|
||||
private:
|
||||
subprocess_s proc {};
|
||||
std::atomic<bool> is_created{false};
|
||||
|
||||
bool has_handle() const;
|
||||
};
|
||||
@@ -158,6 +158,8 @@ TEXT_MODEL_MAP: dict[str, str] = {
|
||||
"MiniCPMForCausalLM": "minicpm",
|
||||
"MiniCPMV4_6ForConditionalGeneration": "minicpm",
|
||||
"MiniMaxM2ForCausalLM": "minimax",
|
||||
"MiniMaxM3SparseForCausalLM": "minimax",
|
||||
"MiniMaxM3SparseForConditionalGeneration": "minimax",
|
||||
"Ministral3ForCausalLM": "mistral3",
|
||||
"Mistral3ForConditionalGeneration": "mistral3",
|
||||
"MistralForCausalLM": "llama",
|
||||
@@ -165,6 +167,7 @@ TEXT_MODEL_MAP: dict[str, str] = {
|
||||
"ModernBertForMaskedLM": "bert",
|
||||
"ModernBertForSequenceClassification": "bert",
|
||||
"ModernBertModel": "bert",
|
||||
"NanbeigeForCausalLM": "nanbeige",
|
||||
"NemotronForCausalLM": "nemotron",
|
||||
"NemotronHForCausalLM": "nemotron",
|
||||
"NeoBERT": "bert",
|
||||
@@ -267,6 +270,7 @@ MMPROJ_MODEL_MAP: dict[str, str] = {
|
||||
"Gemma4UnifiedForConditionalGeneration": "gemma",
|
||||
"Glm4vForConditionalGeneration": "qwen3vl",
|
||||
"Glm4vMoeForConditionalGeneration": "qwen3vl",
|
||||
"Glm5vForConditionalGeneration": "kimivl",
|
||||
"GlmOcrForConditionalGeneration": "qwen3vl",
|
||||
"GlmasrModel": "ultravox",
|
||||
"Granite4VisionForConditionalGeneration": "granite",
|
||||
@@ -285,6 +289,7 @@ MMPROJ_MODEL_MAP: dict[str, str] = {
|
||||
"LlavaForConditionalGeneration": "llava",
|
||||
"MERaLiON2ForConditionalGeneration": "ultravox",
|
||||
"MiMoV2ForCausalLM": "mimo",
|
||||
"MiniMaxM3SparseForConditionalGeneration": "minimax",
|
||||
"MiniCPMV4_6ForConditionalGeneration": "minicpm",
|
||||
"Mistral3ForConditionalGeneration": "llava",
|
||||
"NemotronH_Nano_VL_V2": "nemotron",
|
||||
|
||||
+2
-2
@@ -1156,7 +1156,7 @@ class TextModel(ModelBase):
|
||||
or "projector." in name or "pre_mm_projector_norm" in name \
|
||||
or "image_newline" in name or "view_seperator" in name \
|
||||
or "patch_embed" in name or "patch_embedding" in name \
|
||||
or "patch_merger." in name or "model.connector." in name:
|
||||
or "patch_merger." in name or "patch_merge_mlp." in name or "model.connector." in name:
|
||||
return None
|
||||
|
||||
return super().filter_tensors(item)
|
||||
@@ -1203,7 +1203,7 @@ class TextModel(ModelBase):
|
||||
self.gguf_writer.add_embedding_length(n_embd)
|
||||
logger.info(f"gguf: embedding length = {n_embd}")
|
||||
|
||||
if (n_ff := self.find_hparam(["prefix_dense_intermediate_size", "intermediate_size", "n_inner", "hidden_dim"], optional=True)) is not None:
|
||||
if (n_ff := self.find_hparam(["prefix_dense_intermediate_size", "dense_intermediate_size", "intermediate_size", "n_inner", "hidden_dim"], optional=True)) is not None:
|
||||
self.gguf_writer.add_feed_forward_length(n_ff)
|
||||
logger.info(f"gguf: feed forward length = {n_ff}")
|
||||
|
||||
|
||||
@@ -152,3 +152,19 @@ class KimiK25Model(MmprojModel):
|
||||
name = name.replace(".proj.2.", ".proj.linear_2.")
|
||||
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
|
||||
|
||||
@ModelBase.register("Glm5vForConditionalGeneration")
|
||||
class Glm5vModel(KimiK25Model):
|
||||
"""GLM-5.2-Vision MoonViT3d encoder and projector
|
||||
|
||||
Uses the same vision encoder and projector as Kimi-K2.5, so it reuses the
|
||||
kimik25 projector type. The image begin/end tokens differ, but they are
|
||||
resolved at runtime from the text model vocab.
|
||||
"""
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
if name.startswith("mm_projector.linear_"):
|
||||
name = name.replace("mm_projector.linear_", "mm_projector.proj.linear_", 1)
|
||||
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
|
||||
+16
-2
@@ -69,9 +69,14 @@ class LlamaModel(TextModel):
|
||||
target_config = {**target_config, **target_config["text_config"]}
|
||||
self.target_vocab_size = target_config["vocab_size"]
|
||||
|
||||
# target_layers: derived from target model layer count (low/mid/high)
|
||||
# target_layers: use the eagle3 config's explicit aux hidden-state layer ids
|
||||
# if present, else derive from the target layer count.
|
||||
target_num_layers = target_config["num_hidden_layers"]
|
||||
target_layers = [2, target_num_layers // 2, target_num_layers - 3]
|
||||
aux_layer_ids = eagle3_raw_config.get("eagle_aux_hidden_state_layer_ids")
|
||||
if aux_layer_ids:
|
||||
target_layers = aux_layer_ids
|
||||
else:
|
||||
target_layers = [2, target_num_layers // 2, target_num_layers - 3]
|
||||
logger.info(f"EAGLE-3: target_layers = {target_layers} (target model has {target_num_layers} layers)")
|
||||
self.gguf_writer.add_target_layers(target_layers)
|
||||
|
||||
@@ -90,6 +95,12 @@ class LlamaModel(TextModel):
|
||||
logger.info(f"EAGLE-3: norm_before_residual = {norm_before_residual}")
|
||||
self.gguf_writer.add_norm_before_residual(norm_before_residual)
|
||||
|
||||
# norm_before_fc: RMSNorm applied to the fused target features before the
|
||||
# fc projection (e.g. nvidia/gpt-oss-120b-Eagle3-v3)
|
||||
norm_before_fc = eagle3_raw_config.get("norm_before_fc", False)
|
||||
logger.info(f"EAGLE-3: norm_before_fc = {norm_before_fc}")
|
||||
self.gguf_writer.add_norm_before_fc(norm_before_fc)
|
||||
|
||||
def set_vocab(self):
|
||||
# eagle3: use tokenizer from target model if provided
|
||||
original_dir_model = None
|
||||
@@ -222,6 +233,9 @@ class LlamaModel(TextModel):
|
||||
if name == "fc.weight":
|
||||
yield (name, data_torch)
|
||||
return
|
||||
if name == "input_norm.weight":
|
||||
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ENC_OUTPUT_NORM), data_torch)
|
||||
return
|
||||
if name == "d2t":
|
||||
# store for manual int64 handling in prepare_tensors (avoid F32 conversion)
|
||||
if not hasattr(self, '_eagle3_int_tensors'):
|
||||
|
||||
+114
-9
@@ -1,8 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
from typing import Callable, TYPE_CHECKING
|
||||
from typing import Any, Callable, Iterable, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
@@ -229,7 +230,13 @@ class MimoV2Model(TextModel):
|
||||
|
||||
|
||||
@ModelBase.register("MiMoV2ForCausalLM")
|
||||
class MiMoV2VisionModel(MmprojModel):
|
||||
class MiMoV2VisionAudioModel(MmprojModel):
|
||||
has_audio_encoder = True
|
||||
|
||||
_audio_tok_hparams: dict[str, Any] | None = None
|
||||
_rvq_codebook_sizes: list[int] | None = None
|
||||
_code_embd: dict[int, Tensor] | None = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
assert self.hparams_vision is not None
|
||||
@@ -253,10 +260,22 @@ class MiMoV2VisionModel(MmprojModel):
|
||||
self.visual_token_window_size = int(hp.get("visual_token_window_size", -1))
|
||||
self.use_sink = bool(hp.get("use_sink", False))
|
||||
|
||||
def get_audio_config(self) -> dict[str, Any] | None:
|
||||
if self._audio_tok_hparams is None:
|
||||
path = self.dir_model / "audio_tokenizer" / "config.json"
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
# aliases so MmprojModel.find_aparam() / n_block_keys can resolve them
|
||||
cfg["hidden_size"] = cfg["d_model"]
|
||||
cfg["intermediate_size"] = cfg["encoder_ffn_dim"]
|
||||
cfg["num_attention_heads"] = cfg["encoder_attention_heads"]
|
||||
self._audio_tok_hparams = cfg
|
||||
return self._audio_tok_hparams
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
|
||||
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.MIMOVL)
|
||||
self.gguf_writer.add_clip_vision_projector_type(gguf.VisionProjectorType.MIMOVL)
|
||||
self.gguf_writer.add_vision_use_silu(True)
|
||||
self.gguf_writer.add_vision_head_count_kv(self.num_kv_heads)
|
||||
self.gguf_writer.add_vision_spatial_merge_size(self.spatial_merge_size)
|
||||
@@ -266,19 +285,45 @@ class MiMoV2VisionModel(MmprojModel):
|
||||
self.gguf_writer.add_vision_min_pixels(int(self.preprocessor_config["min_pixels"]))
|
||||
self.gguf_writer.add_vision_max_pixels(int(self.preprocessor_config["max_pixels"]))
|
||||
|
||||
assert self.hparams_audio is not None
|
||||
self.gguf_writer.add_clip_audio_projector_type(gguf.VisionProjectorType.MIMO_AUDIO)
|
||||
self.gguf_writer.add_audio_num_mel_bins(self.hparams_audio["n_mels"])
|
||||
self.gguf_writer.add_audio_attention_layernorm_eps(self.hparams_audio.get("layer_norm_eps", 1e-5))
|
||||
|
||||
assert self._rvq_codebook_sizes is not None
|
||||
self.gguf_writer.add_audio_rvq_num_quantizers(len(self._rvq_codebook_sizes))
|
||||
self.gguf_writer.add_audio_rvq_codebook_size(self._rvq_codebook_sizes)
|
||||
|
||||
n_layer = self.hparams_audio["encoder_layers"]
|
||||
swa_per_block = self.hparams_audio.get("swa_per_block", 1)
|
||||
if self.hparams_audio.get("hybrid_attention") and swa_per_block > 1:
|
||||
wa_pattern = [0 if i % swa_per_block < swa_per_block - 1 else -1 for i in range(n_layer)]
|
||||
else:
|
||||
wa_pattern = [-1] * n_layer
|
||||
self.gguf_writer.add_audio_wa_pattern_mode(wa_pattern)
|
||||
self.gguf_writer.add_audio_window_size(int(self.hparams_audio["encoder_attn_window_size"][0]))
|
||||
|
||||
audio_cfg = self.global_config["audio_config"]
|
||||
self.gguf_writer.add_audio_local_block_count(int(audio_cfg["input_local_layers"]))
|
||||
self.gguf_writer.add_audio_local_group_size(int(audio_cfg["group_size"]))
|
||||
|
||||
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
||||
# Sinks must be F32: any sink-style softmax/mask add in ggml requires
|
||||
# F32, and we fold sinks into a host-built F32 mask at encode time.
|
||||
if new_name.endswith(".attn_sinks"):
|
||||
# for audio encoder: keep codebook in F32
|
||||
if new_name in (
|
||||
gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.A_ENC_RVQ_CODEBOOK] + ".weight",
|
||||
gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.A_MM_CODE_EMBD] + ".weight",
|
||||
):
|
||||
return gguf.GGMLQuantizationType.F32
|
||||
if ("encoder.conv" in name or "encoder.down_sample_layer" in name) and name.endswith(".weight"):
|
||||
return gguf.GGMLQuantizationType.F32
|
||||
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
||||
name, _ = item
|
||||
if not name.startswith("visual."):
|
||||
return None
|
||||
return super().filter_tensors(item)
|
||||
if name.startswith("visual.") or name.startswith("speech_embeddings.") or name.startswith("audio_encoder."):
|
||||
return super().filter_tensors(item)
|
||||
return None
|
||||
|
||||
def modify_tensors(self, data_torch, name, bid):
|
||||
# Conv3D patch embed: split along the temporal axis (kt=2) into two Conv2D
|
||||
@@ -292,4 +337,64 @@ class MiMoV2VisionModel(MmprojModel):
|
||||
yield (embd_name + ".weight.1", data_torch[:, :, 1, ...])
|
||||
return
|
||||
|
||||
if m := re.match(r"^speech_embeddings\.(\d+)\.weight$", name):
|
||||
if self._code_embd is None:
|
||||
self._code_embd = {}
|
||||
self._code_embd[int(m.group(1))] = data_torch
|
||||
|
||||
n_channels = int(self.global_config["audio_config"]["audio_channels"])
|
||||
if len(self._code_embd) < n_channels:
|
||||
return
|
||||
merged = torch.stack([self._code_embd.pop(i) for i in range(n_channels)], dim=0)
|
||||
yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_MM_CODE_EMBD), merged)
|
||||
return
|
||||
|
||||
if "conv1.bias" in name or "conv2.bias" in name:
|
||||
# transpose conv1/conv2 bias so it broadcasts against [n_frames, C_out, 1]
|
||||
data_torch = data_torch.unsqueeze(-1)
|
||||
|
||||
if name == "audio_encoder.projection.mlp.0.weight":
|
||||
yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_MMPROJ, 1), data_torch)
|
||||
return
|
||||
if name == "audio_encoder.projection.mlp.2.weight":
|
||||
yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_MMPROJ, 2), data_torch)
|
||||
return
|
||||
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
|
||||
def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
|
||||
# note: audio encoder is in its own subdir "audio_tokenizer"
|
||||
from safetensors.torch import load_file
|
||||
|
||||
tok_dir = self.dir_model / "audio_tokenizer"
|
||||
state_dict = load_file(tok_dir / "model.safetensors")
|
||||
|
||||
codebook_re = re.compile(r"^encoder\.quantizer\.vq\.layers\.(\d+)\._codebook\.embed$")
|
||||
codebooks: dict[int, Tensor] = {}
|
||||
|
||||
# EMA/training-only RVQ buffers - not needed for inference (nearest-codebook
|
||||
# lookup only reads "_codebook.embed")
|
||||
skip_suffixes = (
|
||||
"_codebook.cluster_size",
|
||||
"_codebook.embed_avg",
|
||||
"_codebook.inited",
|
||||
)
|
||||
for name, tensor in state_dict.items():
|
||||
if name.endswith(skip_suffixes):
|
||||
continue
|
||||
if m := codebook_re.match(name):
|
||||
codebooks[int(m.group(1))] = tensor
|
||||
continue
|
||||
yield name, tensor
|
||||
|
||||
# gather codebooks and merge into 3D tensor, similar to MoE MLP tensors
|
||||
n_q = len(codebooks)
|
||||
ordered = [codebooks[i] for i in range(n_q)]
|
||||
self._rvq_codebook_sizes = [int(cb.shape[0]) for cb in ordered]
|
||||
max_bins = max(self._rvq_codebook_sizes)
|
||||
dim = ordered[0].shape[1]
|
||||
merged = ordered[0].new_zeros(n_q, max_bins, dim)
|
||||
for i, cb in enumerate(ordered):
|
||||
merged[i, : cb.shape[0], :] = cb
|
||||
|
||||
yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_ENC_RVQ_CODEBOOK), merged)
|
||||
|
||||
+117
-2
@@ -7,7 +7,7 @@ import torch
|
||||
if TYPE_CHECKING:
|
||||
from torch import Tensor
|
||||
|
||||
from .base import ModelBase, TextModel, gguf
|
||||
from .base import ModelBase, TextModel, MmprojModel, gguf
|
||||
|
||||
|
||||
@ModelBase.register("MiniMaxM2ForCausalLM")
|
||||
@@ -23,7 +23,7 @@ class MiniMaxM2Model(TextModel):
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None):
|
||||
# merge expert weights
|
||||
if 'experts' in name:
|
||||
if "block_sparse_moe.experts." in name:
|
||||
n_experts = self.find_hparam(["num_local_experts", "num_experts"])
|
||||
assert bid is not None
|
||||
|
||||
@@ -52,3 +52,118 @@ class MiniMaxM2Model(TextModel):
|
||||
return
|
||||
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
|
||||
|
||||
@ModelBase.register("MiniMaxM3SparseForCausalLM", "MiniMaxM3SparseForConditionalGeneration")
|
||||
class MiniMaxM3Model(MiniMaxM2Model):
|
||||
model_arch = gguf.MODEL_ARCH.MINIMAXM3
|
||||
|
||||
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
||||
if ".indexer." in new_name:
|
||||
return gguf.GGMLQuantizationType.F32
|
||||
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
|
||||
self.gguf_writer.add_expert_shared_count(self.find_hparam(["n_shared_experts"]))
|
||||
self.gguf_writer.add_expert_weights_scale(self.find_hparam(["routed_scaling_factor"]))
|
||||
self.gguf_writer.add_expert_weights_norm(True)
|
||||
|
||||
sac = self.find_hparam(["sparse_attention_config"])
|
||||
self.gguf_writer.add_indexer_head_count(sac["sparse_num_index_heads"])
|
||||
self.gguf_writer.add_indexer_key_length(sac["sparse_index_dim"])
|
||||
self.gguf_writer.add_indexer_top_k(sac["sparse_topk_blocks"])
|
||||
self.gguf_writer.add_indexer_block_size(sac["sparse_block_size"])
|
||||
self.gguf_writer.add_indexer_local_blocks(sac["sparse_local_block"])
|
||||
|
||||
moe_layer_freq = self.find_hparam(["moe_layer_freq"])
|
||||
n_dense = 0
|
||||
for v in moe_layer_freq:
|
||||
if v == 0:
|
||||
n_dense += 1
|
||||
else:
|
||||
break
|
||||
self.gguf_writer.add_leading_dense_block_count(n_dense)
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None):
|
||||
# Gemma-style (1 + w) RMSNorm: bake the +1 in so llama.cpp can use plain RMSNorm
|
||||
if name.endswith("norm.weight"):
|
||||
data_torch = data_torch + 1.0
|
||||
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
|
||||
|
||||
@ModelBase.register("MiniMaxM3SparseForConditionalGeneration", "MiniMaxM3VLForConditionalGeneration")
|
||||
class MiniMaxM3VisionModel(MmprojModel):
|
||||
@classmethod
|
||||
def filter_tensors(cls, item):
|
||||
name, gen = item
|
||||
# keep only the vision-side tensors; text / mtp / sparse-index are dropped
|
||||
if not name.startswith(("vision_tower.", "multi_modal_projector.", "patch_merge_mlp.")):
|
||||
return None
|
||||
return super().filter_tensors((name, gen))
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
assert self.hparams_vision is not None
|
||||
|
||||
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.MINIMAXM3)
|
||||
self.gguf_writer.add_vision_use_gelu(True)
|
||||
|
||||
# the ViT carries its own LayerNorm eps (text tower uses a different one)
|
||||
self.gguf_writer.add_vision_attention_layernorm_eps(
|
||||
self.hparams_vision.get("layer_norm_eps", 1e-5)
|
||||
)
|
||||
|
||||
comp = self.hparams_vision.get("img_token_compression_config", {})
|
||||
merge_size = comp.get("spatial_merge_size", 2)
|
||||
self.gguf_writer.add_vision_spatial_merge_size(int(merge_size))
|
||||
|
||||
def modify_tensors(self, data_torch, name, bid):
|
||||
assert self.hparams_vision is not None
|
||||
|
||||
# Conv3d patch embed -> Conv2d slices
|
||||
if name == "vision_tower.vision_model.embeddings.patch_embedding.weight":
|
||||
if data_torch.ndim != 5:
|
||||
raise ValueError(f"unexpected patch_embedding rank {data_torch.ndim} for {name}")
|
||||
kt = data_torch.shape[2]
|
||||
base = gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_ENC_EMBD_PATCH]
|
||||
for t in range(kt):
|
||||
suffix = ".weight" if t == 0 else f".weight.{t}"
|
||||
yield (base + suffix, data_torch[:, :, t, ...])
|
||||
return
|
||||
|
||||
# Permute ViT q/k. HF [Ta Ha Wa | Tb Hb Wb | pad] reorder to [Ta Tb | Ha Hb | Wa Wb | pad].
|
||||
for new_name, tensor in super().modify_tensors(data_torch, name, bid):
|
||||
if ".attn_q." in new_name or ".attn_k." in new_name:
|
||||
tensor = self._permute_vit_qk(tensor, new_name)
|
||||
yield new_name, tensor
|
||||
|
||||
def _permute_vit_qk(self, t: "Tensor", new_name: str) -> "Tensor":
|
||||
assert self.hparams_vision is not None
|
||||
n_head = self.hparams_vision["num_attention_heads"]
|
||||
d_head = t.shape[0] // n_head
|
||||
axis_dim = 2 * ((2 * (d_head // 2) // 3) // 2)
|
||||
ah = axis_dim // 2
|
||||
half = 3 * ah
|
||||
perm = []
|
||||
perm += list(range(0, ah))
|
||||
perm += list(range(half, half + ah))
|
||||
perm += list(range(ah, 2 * ah))
|
||||
perm += list(range(half + ah, half + 2 * ah))
|
||||
perm += list(range(2 * ah, 3 * ah))
|
||||
perm += list(range(half + 2 * ah, half + 3 * ah))
|
||||
perm += list(range(2 * half, d_head))
|
||||
|
||||
assert axis_dim % 2 == 0
|
||||
assert 3 * axis_dim <= d_head
|
||||
assert len(perm) == d_head
|
||||
assert sorted(perm) == list(range(d_head)), "perm is not a bijection of d_head"
|
||||
assert t.shape[0] == n_head * d_head, f"{new_name}: {t.shape[0]} != {n_head}*{d_head}"
|
||||
assert d_head == 80
|
||||
|
||||
idx = torch.tensor(perm, dtype=torch.long)
|
||||
if t.ndim == 2:
|
||||
return t.reshape(n_head, d_head, t.shape[1])[:, idx, :].reshape(t.shape)
|
||||
return t.reshape(n_head, d_head)[:, idx].reshape(t.shape)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import ModelBase, gguf, logger
|
||||
from .llama import LlamaModel
|
||||
|
||||
|
||||
@ModelBase.register("NanbeigeForCausalLM")
|
||||
class NanbeigeModel(LlamaModel):
|
||||
model_arch = gguf.MODEL_ARCH.NANBEIGE
|
||||
undo_permute = True
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
hparams = self.hparams
|
||||
|
||||
n_loops = int(hparams.get("num_loops", 1) or 1)
|
||||
if n_loops < 1:
|
||||
n_loops = 1
|
||||
self.gguf_writer.add_num_loops(n_loops)
|
||||
logger.info(f"gguf: num_loops = {n_loops}")
|
||||
|
||||
skip_loop_final_norm = bool(hparams.get("skip_loop_final_norm", False))
|
||||
self.gguf_writer.add_skip_loop_final_norm(skip_loop_final_norm)
|
||||
logger.info(f"gguf: skip_loop_final_norm = {skip_loop_final_norm}")
|
||||
@@ -144,6 +144,8 @@ Examples:
|
||||
- Gemma 3 folds the `1 +` of its `norm(1 + weight)` normalization into the weights at conversion time, so the graph just does a plain RMS norm.
|
||||
- Qwen3-Next applies its tensor permutation during conversion (in `modify_tensors`), so the graph can consume the already-permuted weights directly.
|
||||
|
||||
Exception: a plain `weight * scale` with a constant scale is usually better left to inference time rather than folded into the weight at conversion. The scale conceptually applies to the activation, not the weight, so folding it into the weight can hurt numerical stability, and it shifts the weight's value range in a way that can make quantization worse. In this case, write the scale to GGUF as its own metadata key (e.g. `%s.attention.output_scale`, `%s.attention.value_scale`, `%s.embedding_scale`) and apply it in the graph, instead of pre-multiplying the weight tensor during conversion.
|
||||
|
||||
### Working with ggml_rope_ext
|
||||
|
||||
PyTorch implementations usually prefer explicitly calculating `freq_cis`/`sin`/`cos` components. However, in llama.cpp, most RoPE operations can be handled via `ggml_rope_ext`, which does not require a sin/cos matrix. This saves memory while allowing the GGML RoPE kernel to be fused with other ops.
|
||||
|
||||
+26
-17
@@ -906,26 +906,35 @@ static int ggml_backend_sched_backend_id_from_cur(ggml_backend_sched_t sched, st
|
||||
}
|
||||
|
||||
// operations with weights are preferably run on the same backend as the weights
|
||||
for (int i = 0; i < GGML_MAX_SRC; i++) {
|
||||
const struct ggml_tensor * src = tensor->src[i];
|
||||
if (src == NULL) {
|
||||
continue;
|
||||
}
|
||||
// skip ROPE since the rope freqs tensor is too small to choose a backend based on it
|
||||
// not an ideal solution
|
||||
if (tensor->op != GGML_OP_ROPE && src->buffer != NULL && src->buffer->usage == GGML_BACKEND_BUFFER_USAGE_WEIGHTS) {
|
||||
int src_backend_id = ggml_backend_sched_backend_from_buffer(sched, src, tensor);
|
||||
// check if a backend with higher prio wants to offload the op
|
||||
if (sched->op_offload && src_backend_id == sched->n_backends - 1 && ggml_backend_buffer_is_host(src->buffer)) {
|
||||
for (int b = 0; b < src_backend_id; b++) {
|
||||
if (ggml_backend_supports_op(sched->backends[b], tensor) && ggml_backend_offload_op(sched->backends[b], tensor)) {
|
||||
SET_CAUSE(tensor, "1.off");
|
||||
return b;
|
||||
// TODO: there are exceptions (see below) - not an ideal solution
|
||||
bool allow = true;
|
||||
|
||||
// skip ROPE since the rope freqs tensor is too small to choose a backend based on it
|
||||
allow = allow && tensor->op != GGML_OP_ROPE;
|
||||
|
||||
// skip FLASH_ATTN_EXT since the sinks tensor is too small to choose a based based on it
|
||||
allow = allow && tensor->op != GGML_OP_FLASH_ATTN_EXT;
|
||||
|
||||
if (allow) {
|
||||
for (int i = 0; i < GGML_MAX_SRC; i++) {
|
||||
const struct ggml_tensor * src = tensor->src[i];
|
||||
if (src == NULL) {
|
||||
continue;
|
||||
}
|
||||
if (src->buffer != NULL && src->buffer->usage == GGML_BACKEND_BUFFER_USAGE_WEIGHTS) {
|
||||
int src_backend_id = ggml_backend_sched_backend_from_buffer(sched, src, tensor);
|
||||
// check if a backend with higher prio wants to offload the op
|
||||
if (sched->op_offload && src_backend_id == sched->n_backends - 1 && ggml_backend_buffer_is_host(src->buffer)) {
|
||||
for (int b = 0; b < src_backend_id; b++) {
|
||||
if (ggml_backend_supports_op(sched->backends[b], tensor) && ggml_backend_offload_op(sched->backends[b], tensor)) {
|
||||
SET_CAUSE(tensor, "1.off");
|
||||
return b;
|
||||
}
|
||||
}
|
||||
}
|
||||
SET_CAUSE(tensor, "1.wgt%d", i);
|
||||
return src_backend_id;
|
||||
}
|
||||
SET_CAUSE(tensor, "1.wgt%d", i);
|
||||
return src_backend_id;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1797,14 +1797,6 @@ class tinyBLAS_Q0_AVX {
|
||||
//PPC Implementation
|
||||
#if defined(__MMA__)
|
||||
|
||||
#define SAVE_ACC(ACC, ii, jj) \
|
||||
__builtin_mma_disassemble_acc(vec_C, ACC); \
|
||||
for (int I = 0; I < 4; I++) { \
|
||||
for (int J = 0; J < 4; J++) { \
|
||||
*((float*)(C+ii+((jj+J)*ldc)+I)) = *((float*)&vec_C[I]+J); \
|
||||
} \
|
||||
} \
|
||||
|
||||
template<typename T>
|
||||
struct mma_instr;
|
||||
|
||||
@@ -1834,10 +1826,49 @@ class tinyBLAS_HP16_PPC {
|
||||
}
|
||||
|
||||
void matmul(int64_t m, int64_t n) {
|
||||
mnpack(0, m, 0, n);
|
||||
int64_t mc = 256;
|
||||
int64_t nc = 256;
|
||||
int64_t kc = 256;
|
||||
#if defined(_AIX) || defined(__BIG_ENDIAN__)
|
||||
mc = 128;
|
||||
nc = 128;
|
||||
kc = 128;
|
||||
#endif
|
||||
if (k < kc) {
|
||||
kc = k;
|
||||
}
|
||||
bool can_use_tiled = (m % mc == 0) && (n % nc == 0) && (k % kc == 0);
|
||||
if (can_use_tiled) {
|
||||
matmul_tiled(m, n, mc, nc, kc);
|
||||
} else {
|
||||
mnpack(0, m, 0, n);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
__attribute__((always_inline))
|
||||
inline void save_acc(acc_t * ACC, int64_t ii, int64_t jj) {
|
||||
vec_t vec_C[4];
|
||||
__builtin_mma_disassemble_acc(vec_C, ACC);
|
||||
for (int I = 0; I < 4; I++) {
|
||||
for (int J = 0; J < 4; J++) {
|
||||
*((float *)(C+ii+((jj+J)*ldc)+I)) = *((float *)&vec_C[I]+J);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((always_inline))
|
||||
inline void add_save_acc(acc_t * ACC, int64_t ii, int64_t jj) {
|
||||
vec_t vec_C[4];
|
||||
__builtin_mma_disassemble_acc(vec_C, ACC);
|
||||
for (int I = 0; I < 4; I++) {
|
||||
for (int J = 0; J < 4; J++) {
|
||||
float * c_ptr = (float *)(C+ii+((jj+J)*ldc)+I);
|
||||
*c_ptr += *((float *)&vec_C[I]+J);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void vector_permute_store(vec_t *c, int numVec, unsigned char *vecOffset) {
|
||||
vec_t t[8], s[8];
|
||||
vec_t swiz1 = {0, 1, 2, 3, 16, 17, 18, 19, 4, 5, 6, 7, 20, 21, 22, 23};
|
||||
@@ -1896,6 +1927,7 @@ class tinyBLAS_HP16_PPC {
|
||||
j = (rows >> 3);
|
||||
if (j > 0) {
|
||||
do {
|
||||
aoffsets[0] = aoffset;
|
||||
if (cols == 4) {
|
||||
aoffsets[0] = aoffset;
|
||||
for (int it = 1; it < 4; ++it)
|
||||
@@ -1910,17 +1942,17 @@ class tinyBLAS_HP16_PPC {
|
||||
}
|
||||
i = (cols >> 3);
|
||||
if (i > 0) {
|
||||
aoffsets[0] = aoffset;
|
||||
for (int it = 1; it < 8; ++it) {
|
||||
aoffsets[it] = aoffsets[it-1] + lda;
|
||||
}
|
||||
aoffset += 8 * lda;
|
||||
|
||||
do {
|
||||
for (int it = 0; it < 8; ++it)
|
||||
c_arr[it] = vec_xl(0, (vector unsigned char*)aoffsets[it]);
|
||||
vector_permute_store(c_arr, 8, vecOffset);
|
||||
for (int it = 0; it < 8; ++it)
|
||||
aoffsets[it] = aoffsets[it] + 8*lda;
|
||||
aoffsets[it] = aoffsets[it] + 8;
|
||||
vecOffset += 128;
|
||||
i--;
|
||||
} while(i > 0);
|
||||
@@ -2147,8 +2179,8 @@ class tinyBLAS_HP16_PPC {
|
||||
mma_instr<TA>::outer_product(&acc_1, vec_A[x], vec_B[x+4]);
|
||||
}
|
||||
}
|
||||
SAVE_ACC(&acc_0, ii, jj);
|
||||
SAVE_ACC(&acc_1, ii, jj+4);
|
||||
save_acc(&acc_0, ii, jj);
|
||||
save_acc(&acc_1, ii, jj+4);
|
||||
}
|
||||
|
||||
void KERNEL_8x4(int64_t ii, int64_t jj) {
|
||||
@@ -2164,8 +2196,8 @@ class tinyBLAS_HP16_PPC {
|
||||
mma_instr<TA>::outer_product(&acc_1, vec_A[x+4], vec_B[x]);
|
||||
}
|
||||
}
|
||||
SAVE_ACC(&acc_0, ii, jj);
|
||||
SAVE_ACC(&acc_1, ii+4, jj);
|
||||
save_acc(&acc_0, ii, jj);
|
||||
save_acc(&acc_1, ii+4, jj);
|
||||
}
|
||||
|
||||
|
||||
@@ -2186,13 +2218,64 @@ class tinyBLAS_HP16_PPC {
|
||||
mma_instr<TA>::outer_product(&acc_3, vec_A[x+4], vec_B[x+4]);
|
||||
}
|
||||
}
|
||||
|
||||
SAVE_ACC(&acc_0, ii, jj);
|
||||
SAVE_ACC(&acc_1, ii, jj+4);
|
||||
SAVE_ACC(&acc_2, ii+4, jj);
|
||||
SAVE_ACC(&acc_3, ii+4, jj+4);
|
||||
save_acc(&acc_0, ii, jj);
|
||||
save_acc(&acc_1, ii, jj+4);
|
||||
save_acc(&acc_2, ii+4, jj);
|
||||
save_acc(&acc_3, ii+4, jj+4);
|
||||
}
|
||||
|
||||
inline void MMA_16x8(vec_t * vec_A0, vec_t * vec_A1, vec_t * vec_B, acc_t * acc) {
|
||||
for (int x = 0; x < 4; x ++) {
|
||||
mma_instr<TA>::outer_product(&acc[0], vec_A0[x], vec_B[x]);
|
||||
mma_instr<TA>::outer_product(&acc[1], vec_A0[x], vec_B[x+4]);
|
||||
mma_instr<TA>::outer_product(&acc[2], vec_A0[x+4], vec_B[x]);
|
||||
mma_instr<TA>::outer_product(&acc[3], vec_A0[x+4], vec_B[x+4]);
|
||||
mma_instr<TA>::outer_product(&acc[4], vec_A1[x], vec_B[x]);
|
||||
mma_instr<TA>::outer_product(&acc[5], vec_A1[x], vec_B[x+4]);
|
||||
mma_instr<TA>::outer_product(&acc[6], vec_A1[x+4], vec_B[x]);
|
||||
mma_instr<TA>::outer_product(&acc[7], vec_A1[x+4], vec_B[x+4]);
|
||||
}
|
||||
}
|
||||
void KERNEL(int64_t ii, int64_t jj, int64_t mc, int64_t nc, int64_t kc, vec_t * vec_A, vec_t * vec_B, int64_t kk) {
|
||||
for (int64_t i = 0; i < mc; i += 16) {
|
||||
int A_base_addr = (mc / 8) * (i / 8) * 8;
|
||||
for (int64_t j = 0; j < nc; j += 8) {
|
||||
int B_base_addr = (nc / 8) * (j / 8) * 8;
|
||||
acc_t acc[8];
|
||||
vec_t A0_block[8]; vec_t A1_block[8];
|
||||
for (int x = 0; x < 8; x++)
|
||||
__builtin_mma_xxsetaccz(&acc[x]);
|
||||
for (int64_t l = 0; l < kc; l += 8) {
|
||||
int A0_block_idx = A_base_addr + (l / 8) * 8;
|
||||
int A1_block_idx = A0_block_idx + (mc / 8) * 8;
|
||||
int B_block_idx = B_base_addr + (l / 8) * 8;
|
||||
vec_t* A0_block = &vec_A[A0_block_idx];
|
||||
vec_t* A1_block = &vec_A[A1_block_idx];
|
||||
vec_t* B_block = &vec_B[B_block_idx];
|
||||
MMA_16x8(A0_block, A1_block, B_block, acc);
|
||||
}
|
||||
if (kk == 0) {
|
||||
save_acc(&acc[0], ii + i, jj + j);
|
||||
save_acc(&acc[1], ii + i, jj + j + 4);
|
||||
save_acc(&acc[2], ii + i + 4, jj + j);
|
||||
save_acc(&acc[3], ii + i + 4, jj + j + 4);
|
||||
save_acc(&acc[4], ii + i + 8, jj + j);
|
||||
save_acc(&acc[5], ii + i + 8, jj + j + 4);
|
||||
save_acc(&acc[6], ii + i + 12, jj + j);
|
||||
save_acc(&acc[7], ii + i + 12, jj + j + 4);
|
||||
} else {
|
||||
add_save_acc(&acc[0], ii + i, jj + j);
|
||||
add_save_acc(&acc[1], ii + i, jj + j + 4);
|
||||
add_save_acc(&acc[2], ii + i + 4, jj + j);
|
||||
add_save_acc(&acc[3], ii + i + 4, jj + j + 4);
|
||||
add_save_acc(&acc[4], ii + i + 8, jj + j);
|
||||
add_save_acc(&acc[5], ii + i + 8, jj + j + 4);
|
||||
add_save_acc(&acc[6], ii + i + 12, jj + j);
|
||||
add_save_acc(&acc[7], ii + i + 12, jj + j + 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
template<int RM, int RN>
|
||||
void gemm_small(int64_t m0, int64_t m, int64_t n0, int64_t n) {
|
||||
int64_t ytiles = (m - m0) / RM;
|
||||
@@ -2281,6 +2364,29 @@ class tinyBLAS_HP16_PPC {
|
||||
}
|
||||
}
|
||||
|
||||
void matmul_tiled(int64_t m, int64_t n, int64_t mc, int64_t nc, int64_t kc) {
|
||||
int64_t ytiles = m / mc;
|
||||
int64_t xtiles = n / nc;
|
||||
int64_t tiles = xtiles * ytiles;
|
||||
int64_t duty = (tiles + nth - 1) / nth;
|
||||
int64_t start = duty * ith;
|
||||
int64_t end = start + duty;
|
||||
if (end > tiles) {
|
||||
end = tiles;
|
||||
}
|
||||
for (int64_t job = start; job < end; ++job) {
|
||||
int64_t ii = (job / xtiles) * mc;
|
||||
int64_t jj = (job % xtiles) * nc;
|
||||
for (int64_t kk = 0; kk < k; kk += kc) {
|
||||
vec_t A_pack[kc * mc / 8];
|
||||
vec_t B_pack[kc * nc / 8];
|
||||
packNormal(A + (ii * lda) + kk, lda, kc, mc, (uint8_t *)A_pack);
|
||||
packNormal(B + (jj * ldb) + kk, ldb, kc, nc, (uint8_t *)B_pack);
|
||||
KERNEL(ii, jj, mc, nc, kc, A_pack, B_pack, kk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <int RM, int RN>
|
||||
NOINLINE void gemm(int64_t m0, int64_t m, int64_t n0, int64_t n) {
|
||||
int64_t ytiles = (m - m0) / RM;
|
||||
|
||||
@@ -154,5 +154,3 @@ if (GGML_HIP_RCCL)
|
||||
endif()
|
||||
|
||||
target_link_libraries(ggml-hip PRIVATE ggml-base hip::host roc::rocblas roc::hipblas)
|
||||
|
||||
target_compile_options(ggml-hip PRIVATE "$<$<COMPILE_LANGUAGE:HIP>:-ffast-math;-fno-finite-math-only>")
|
||||
|
||||
@@ -12772,7 +12772,7 @@ static void ggml_opencl_op_rms_norm_fused(ggml_backend_t backend, ggml_tensor *
|
||||
ggml_tensor_extra_cl * extrad = (ggml_tensor_extra_cl *)dst->extra;
|
||||
|
||||
cl_ulong offset0 = extra0->offset + src0->view_offs;
|
||||
cl_ulong offset1 = extra1->offset + src0->view_offs;
|
||||
cl_ulong offset1 = extra1->offset + src1->view_offs;
|
||||
cl_ulong offsetd = extrad->offset + dst->view_offs;
|
||||
|
||||
ggml_backend_opencl_context *backend_ctx = (ggml_backend_opencl_context *)backend->context;
|
||||
|
||||
@@ -199,9 +199,20 @@ if (GGML_SYCL_DEVICE_ARCH)
|
||||
-fsycl-targets=spir64_gen
|
||||
"SHELL:-Xsycl-target-backend=spir64_gen \"-device ${GGML_SYCL_DEVICE_ARCH}\""
|
||||
)
|
||||
|
||||
# Pass through parallel job (process) count for parallelising the
|
||||
# `llvm-foreach -- ocloc` invocation for compiling AOT device images.
|
||||
include(ProcessorCount)
|
||||
ProcessorCount(_ggml_sycl_nproc)
|
||||
if (_ggml_sycl_nproc LESS 1)
|
||||
set(_ggml_sycl_nproc 1)
|
||||
endif()
|
||||
set(GGML_SYCL_MAX_PARALLEL_LINK_JOBS ${_ggml_sycl_nproc} CACHE STRING
|
||||
"Parallel ocloc jobs for spir64_gen AOT device-image lowering")
|
||||
target_link_options(
|
||||
ggml-sycl PRIVATE
|
||||
-fsycl-targets=spir64_gen
|
||||
"SHELL:-Xsycl-target-backend=spir64_gen \"-device ${GGML_SYCL_DEVICE_ARCH}\""
|
||||
-fsycl-max-parallel-link-jobs=${GGML_SYCL_MAX_PARALLEL_LINK_JOBS}
|
||||
)
|
||||
endif()
|
||||
|
||||
+120
-1
@@ -145,6 +145,8 @@ class Keys:
|
||||
TOKEN_SHIFT_COUNT = "{arch}.token_shift_count"
|
||||
INTERLEAVE_MOE_LAYER_STEP = "{arch}.interleave_moe_layer_step"
|
||||
FULL_ATTENTION_INTERVAL = "{arch}.full_attention_interval"
|
||||
NUM_LOOPS = "{arch}.num_loops"
|
||||
SKIP_LOOP_FINAL_NORM = "{arch}.skip_loop_final_norm"
|
||||
HASH_LAYER_COUNT = "{arch}.hash_layer_count"
|
||||
ACTIVATION_SPARSITY_SCALE = "{arch}.activation_sparsity_scale"
|
||||
ALTUP_ACTIVE_IDX = "{arch}.altup.active_idx"
|
||||
@@ -159,6 +161,7 @@ class Keys:
|
||||
TARGET_HIDDEN_SIZE = "{arch}.target_hidden_size"
|
||||
BLOCK_SIZE = "{arch}.block_size"
|
||||
NORM_BEFORE_RESIDUAL = "{arch}.norm_before_residual"
|
||||
NORM_BEFORE_FC = "{arch}.norm_before_fc"
|
||||
|
||||
class Attention:
|
||||
HEAD_COUNT = "{arch}.attention.head_count"
|
||||
@@ -200,6 +203,8 @@ class Keys:
|
||||
HEAD_COUNT = "{arch}.attention.indexer.head_count"
|
||||
KEY_LENGTH = "{arch}.attention.indexer.key_length"
|
||||
TOP_K = "{arch}.attention.indexer.top_k"
|
||||
BLOCK_SIZE = "{arch}.attention.indexer.block_size" # MSA
|
||||
LOCAL_BLOCKS = "{arch}.attention.indexer.local_blocks" # MSA
|
||||
TYPES = "{arch}.attention.indexer.types"
|
||||
|
||||
class HyperConnection:
|
||||
@@ -372,6 +377,12 @@ class Keys:
|
||||
CONV_KERNEL_SIZE = "clip.audio.conv_kernel_size"
|
||||
MAX_POS_EMB = "clip.audio.max_pos_emb"
|
||||
FEATURE_LAYERS = "clip.audio.feature_layer" # Granite Speech Plus
|
||||
RVQ_NUM_QUANTIZERS = "clip.audio.rvq.num_quantizers"
|
||||
RVQ_CODEBOOK_SIZE = "clip.audio.rvq.codebook_size"
|
||||
WA_PATTERN_MODE = "clip.audio.wa_pattern_mode" # per-layer -1 (full) / 0 (windowed)
|
||||
WINDOW_SIZE = "clip.audio.window_size"
|
||||
LOCAL_BLOCK_COUNT = "clip.audio.local_block_count" # mimo-v2.5: input_local_transformer layer count
|
||||
LOCAL_GROUP_SIZE = "clip.audio.local_group_size" # mimo-v2.5: input_local_transformer grouping size
|
||||
|
||||
class Attention:
|
||||
HEAD_COUNT = "clip.audio.attention.head_count"
|
||||
@@ -528,6 +539,7 @@ class MODEL_ARCH(IntEnum):
|
||||
APERTUS = auto()
|
||||
COGVLM = auto()
|
||||
MINIMAXM2 = auto()
|
||||
MINIMAXM3 = auto()
|
||||
RND1 = auto()
|
||||
PANGU_EMBED = auto()
|
||||
MISTRAL3 = auto()
|
||||
@@ -542,6 +554,7 @@ class MODEL_ARCH(IntEnum):
|
||||
KIMI_LINEAR = auto()
|
||||
TALKIE = auto()
|
||||
MELLUM = auto()
|
||||
NANBEIGE = auto()
|
||||
|
||||
|
||||
class VISION_PROJECTOR_TYPE(IntEnum):
|
||||
@@ -774,6 +787,9 @@ class MODEL_TENSOR(IntEnum):
|
||||
INDEXER_PROJ = auto()
|
||||
INDEXER_ATTN_K = auto()
|
||||
INDEXER_ATTN_Q_B = auto()
|
||||
INDEXER_Q_PROJ = auto()
|
||||
INDEXER_K_PROJ = auto()
|
||||
INDEXER_Q_NORM = auto()
|
||||
INDEXER_COMPRESSOR_WKV = auto()
|
||||
INDEXER_COMPRESSOR_WGATE = auto()
|
||||
INDEXER_COMPRESSOR_APE = auto()
|
||||
@@ -851,6 +867,8 @@ class MODEL_TENSOR(IntEnum):
|
||||
V_MM_UP = auto() # cogvlm
|
||||
V_MM_DOWN = auto() # cogvlm
|
||||
V_MM_GATE = auto() # cogvlm
|
||||
V_MM_MERGER_FC1 = auto() # minimax-m3 (patch-merge MLP)
|
||||
V_MM_MERGER_FC2 = auto() # minimax-m3 (patch-merge MLP)
|
||||
V_TOK_BOI = auto() # cogvlm
|
||||
V_TOK_EOI = auto() # cogvlm
|
||||
V_TOK_IMG_BEGIN = auto() # hunyuanvl
|
||||
@@ -934,6 +952,9 @@ class MODEL_TENSOR(IntEnum):
|
||||
A_ENC_FFN_SCALE_1 = auto() # gemma3n
|
||||
A_ENC_FFN_GATE_1 = auto() # lfm2, gemma3n
|
||||
A_ENC_FFN_DOWN_1 = auto() # lfm2, gemma3n
|
||||
A_ENC_DOWNSAMPLE_CONV = auto() # mimo-audio-tokenizer: post-transformer downsample conv
|
||||
A_ENC_DOWNSAMPLE_NORM = auto() # mimo-audio-tokenizer: post-transformer downsample norm
|
||||
A_ENC_RVQ_CODEBOOK = auto() # mimo-audio-tokenizer: residual vector quantizer codebook, per quantizer index
|
||||
A_MMPROJ = auto()
|
||||
A_MMPROJ_FC = auto()
|
||||
A_MM_NORM_PRE = auto()
|
||||
@@ -942,6 +963,17 @@ class MODEL_TENSOR(IntEnum):
|
||||
A_MM_HARD_EMB_NORM = auto() # gemma3n
|
||||
A_MM_SOFT_EMB_NORM = auto() # gemma3n
|
||||
A_MM_INP_PROJ = auto() # gemma3n
|
||||
A_MM_CODE_EMBD = auto() # mimo: text-side RVQ code embedding table ("text codebook"), merged 3D [n_channels, vocab, dim]
|
||||
A_MM_LOCAL_ATTN_Q = auto() # mimo: input_local_transformer (LLM-side connector)
|
||||
A_MM_LOCAL_ATTN_K = auto()
|
||||
A_MM_LOCAL_ATTN_V = auto()
|
||||
A_MM_LOCAL_ATTN_OUT = auto()
|
||||
A_MM_LOCAL_FFN_GATE = auto()
|
||||
A_MM_LOCAL_FFN_UP = auto()
|
||||
A_MM_LOCAL_FFN_DOWN = auto()
|
||||
A_MM_LOCAL_LN1 = auto()
|
||||
A_MM_LOCAL_LN2 = auto()
|
||||
A_MM_LOCAL_NORM = auto() # final norm after all input_local_transformer layers
|
||||
A_PER_DIM_K_SCALE = auto() # gemma4
|
||||
A_PER_DIM_SCALE = auto() # gemma4
|
||||
# nextn/mtp
|
||||
@@ -1110,6 +1142,7 @@ MODEL_ARCH_NAMES: dict[MODEL_ARCH, str] = {
|
||||
MODEL_ARCH.GROVEMOE: "grovemoe",
|
||||
MODEL_ARCH.APERTUS: "apertus",
|
||||
MODEL_ARCH.MINIMAXM2: "minimax-m2",
|
||||
MODEL_ARCH.MINIMAXM3: "minimax-m3",
|
||||
MODEL_ARCH.COGVLM: "cogvlm",
|
||||
MODEL_ARCH.RND1: "rnd1",
|
||||
MODEL_ARCH.PANGU_EMBED: "pangu-embedded",
|
||||
@@ -1125,6 +1158,7 @@ MODEL_ARCH_NAMES: dict[MODEL_ARCH, str] = {
|
||||
MODEL_ARCH.KIMI_LINEAR: "kimi-linear",
|
||||
MODEL_ARCH.TALKIE: "talkie",
|
||||
MODEL_ARCH.MELLUM: "mellum",
|
||||
MODEL_ARCH.NANBEIGE: "nanbeige",
|
||||
}
|
||||
|
||||
VISION_PROJECTOR_TYPE_NAMES: dict[VISION_PROJECTOR_TYPE, str] = {
|
||||
@@ -1355,6 +1389,9 @@ TENSOR_NAMES: dict[MODEL_TENSOR, str] = {
|
||||
MODEL_TENSOR.INDEXER_PROJ: "blk.{bid}.indexer.proj",
|
||||
MODEL_TENSOR.INDEXER_ATTN_K: "blk.{bid}.indexer.attn_k",
|
||||
MODEL_TENSOR.INDEXER_ATTN_Q_B: "blk.{bid}.indexer.attn_q_b",
|
||||
MODEL_TENSOR.INDEXER_Q_PROJ: "blk.{bid}.indexer.q_proj",
|
||||
MODEL_TENSOR.INDEXER_K_PROJ: "blk.{bid}.indexer.k_proj",
|
||||
MODEL_TENSOR.INDEXER_Q_NORM: "blk.{bid}.indexer.q_norm",
|
||||
MODEL_TENSOR.INDEXER_COMPRESSOR_WKV: "blk.{bid}.indexer_compressor_kv",
|
||||
MODEL_TENSOR.INDEXER_COMPRESSOR_WGATE: "blk.{bid}.indexer_compressor_gate",
|
||||
MODEL_TENSOR.INDEXER_COMPRESSOR_APE: "blk.{bid}.indexer_compressor_ape",
|
||||
@@ -1431,6 +1468,8 @@ TENSOR_NAMES: dict[MODEL_TENSOR, str] = {
|
||||
MODEL_TENSOR.V_MM_UP: "mm.up",
|
||||
MODEL_TENSOR.V_MM_DOWN: "mm.down",
|
||||
MODEL_TENSOR.V_MM_GATE: "mm.gate",
|
||||
MODEL_TENSOR.V_MM_MERGER_FC1: "mm.merger.fc1",
|
||||
MODEL_TENSOR.V_MM_MERGER_FC2: "mm.merger.fc2",
|
||||
MODEL_TENSOR.V_TOK_BOI: "v.boi",
|
||||
MODEL_TENSOR.V_TOK_EOI: "v.eoi",
|
||||
MODEL_TENSOR.V_MM_PRE_NORM: "mm.pre_norm",
|
||||
@@ -1514,6 +1553,9 @@ TENSOR_NAMES: dict[MODEL_TENSOR, str] = {
|
||||
MODEL_TENSOR.A_ENC_FFN_UP_1: "a.blk.{bid}.ffn_up_1",
|
||||
MODEL_TENSOR.A_ENC_FFN_GATE_1: "a.blk.{bid}.ffn_gate_1",
|
||||
MODEL_TENSOR.A_ENC_FFN_DOWN_1: "a.blk.{bid}.ffn_down_1",
|
||||
MODEL_TENSOR.A_ENC_DOWNSAMPLE_CONV: "a.downsample.conv",
|
||||
MODEL_TENSOR.A_ENC_DOWNSAMPLE_NORM: "a.downsample.norm",
|
||||
MODEL_TENSOR.A_ENC_RVQ_CODEBOOK: "a.rvq.codebook",
|
||||
MODEL_TENSOR.A_MMPROJ: "mm.a.mlp.{bid}",
|
||||
MODEL_TENSOR.A_MMPROJ_FC: "mm.a.fc",
|
||||
MODEL_TENSOR.A_MM_NORM_PRE: "mm.a.norm_pre",
|
||||
@@ -1522,6 +1564,17 @@ TENSOR_NAMES: dict[MODEL_TENSOR, str] = {
|
||||
MODEL_TENSOR.A_MM_SOFT_EMB_NORM: "mm.a.soft_emb_norm", # gemma3n
|
||||
MODEL_TENSOR.A_MM_EMBEDDING: "mm.a.embedding", # gemma3n
|
||||
MODEL_TENSOR.A_MM_HARD_EMB_NORM: "mm.a.hard_emb_norm", # gemma3n
|
||||
MODEL_TENSOR.A_MM_CODE_EMBD: "mm.a.code_embd",
|
||||
MODEL_TENSOR.A_MM_LOCAL_ATTN_Q: "mm.a.local_blk.{bid}.attn_q",
|
||||
MODEL_TENSOR.A_MM_LOCAL_ATTN_K: "mm.a.local_blk.{bid}.attn_k",
|
||||
MODEL_TENSOR.A_MM_LOCAL_ATTN_V: "mm.a.local_blk.{bid}.attn_v",
|
||||
MODEL_TENSOR.A_MM_LOCAL_ATTN_OUT: "mm.a.local_blk.{bid}.attn_out",
|
||||
MODEL_TENSOR.A_MM_LOCAL_FFN_GATE: "mm.a.local_blk.{bid}.ffn_gate",
|
||||
MODEL_TENSOR.A_MM_LOCAL_FFN_UP: "mm.a.local_blk.{bid}.ffn_up",
|
||||
MODEL_TENSOR.A_MM_LOCAL_FFN_DOWN: "mm.a.local_blk.{bid}.ffn_down",
|
||||
MODEL_TENSOR.A_MM_LOCAL_LN1: "mm.a.local_blk.{bid}.ln1",
|
||||
MODEL_TENSOR.A_MM_LOCAL_LN2: "mm.a.local_blk.{bid}.ln2",
|
||||
MODEL_TENSOR.A_MM_LOCAL_NORM: "mm.a.local_norm",
|
||||
MODEL_TENSOR.A_PER_DIM_K_SCALE: "a.blk.{bid}.per_dim_k_scale", # gemma4
|
||||
MODEL_TENSOR.A_PER_DIM_SCALE: "a.blk.{bid}.per_dim_scale", # gemma4
|
||||
# lfm2 audio
|
||||
@@ -1627,6 +1680,8 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
|
||||
MODEL_TENSOR.V_RESMPL_QUERY,
|
||||
MODEL_TENSOR.V_TOK_EMBD_IMG_BREAK,
|
||||
MODEL_TENSOR.V_MM_PATCH_MERGER,
|
||||
MODEL_TENSOR.V_MM_MERGER_FC1,
|
||||
MODEL_TENSOR.V_MM_MERGER_FC2,
|
||||
MODEL_TENSOR.V_DS_NORM,
|
||||
MODEL_TENSOR.V_DS_FC1,
|
||||
MODEL_TENSOR.V_DS_FC2,
|
||||
@@ -1721,10 +1776,24 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
|
||||
MODEL_TENSOR.A_ENC_FFN_UP_1,
|
||||
MODEL_TENSOR.A_ENC_FFN_GATE_1,
|
||||
MODEL_TENSOR.A_ENC_FFN_DOWN_1,
|
||||
MODEL_TENSOR.A_ENC_DOWNSAMPLE_CONV,
|
||||
MODEL_TENSOR.A_ENC_DOWNSAMPLE_NORM,
|
||||
MODEL_TENSOR.A_ENC_RVQ_CODEBOOK,
|
||||
MODEL_TENSOR.A_MMPROJ,
|
||||
MODEL_TENSOR.A_MMPROJ_FC,
|
||||
MODEL_TENSOR.A_MM_NORM_PRE,
|
||||
MODEL_TENSOR.A_MM_NORM_MID,
|
||||
MODEL_TENSOR.A_MM_CODE_EMBD,
|
||||
MODEL_TENSOR.A_MM_LOCAL_ATTN_Q,
|
||||
MODEL_TENSOR.A_MM_LOCAL_ATTN_K,
|
||||
MODEL_TENSOR.A_MM_LOCAL_ATTN_V,
|
||||
MODEL_TENSOR.A_MM_LOCAL_ATTN_OUT,
|
||||
MODEL_TENSOR.A_MM_LOCAL_FFN_GATE,
|
||||
MODEL_TENSOR.A_MM_LOCAL_FFN_UP,
|
||||
MODEL_TENSOR.A_MM_LOCAL_FFN_DOWN,
|
||||
MODEL_TENSOR.A_MM_LOCAL_LN1,
|
||||
MODEL_TENSOR.A_MM_LOCAL_LN2,
|
||||
MODEL_TENSOR.A_MM_LOCAL_NORM,
|
||||
MODEL_TENSOR.A_ENC_NORM_CONV,
|
||||
MODEL_TENSOR.A_ENC_LINEAR_POS,
|
||||
MODEL_TENSOR.A_ENC_POS_BIAS_U,
|
||||
@@ -4163,6 +4232,34 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
|
||||
MODEL_TENSOR.FFN_UP_EXP,
|
||||
MODEL_TENSOR.FFN_EXP_PROBS_B,
|
||||
],
|
||||
MODEL_ARCH.MINIMAXM3: [
|
||||
MODEL_TENSOR.TOKEN_EMBD,
|
||||
MODEL_TENSOR.OUTPUT_NORM,
|
||||
MODEL_TENSOR.OUTPUT,
|
||||
MODEL_TENSOR.ATTN_NORM,
|
||||
MODEL_TENSOR.ATTN_Q,
|
||||
MODEL_TENSOR.ATTN_Q_NORM,
|
||||
MODEL_TENSOR.ATTN_K,
|
||||
MODEL_TENSOR.ATTN_K_NORM,
|
||||
MODEL_TENSOR.ATTN_V,
|
||||
MODEL_TENSOR.ATTN_OUT,
|
||||
MODEL_TENSOR.FFN_NORM,
|
||||
MODEL_TENSOR.FFN_GATE_INP,
|
||||
MODEL_TENSOR.FFN_EXP_PROBS_B,
|
||||
MODEL_TENSOR.FFN_GATE_EXP,
|
||||
MODEL_TENSOR.FFN_DOWN_EXP,
|
||||
MODEL_TENSOR.FFN_UP_EXP,
|
||||
MODEL_TENSOR.FFN_GATE_SHEXP,
|
||||
MODEL_TENSOR.FFN_DOWN_SHEXP,
|
||||
MODEL_TENSOR.FFN_UP_SHEXP,
|
||||
MODEL_TENSOR.FFN_GATE,
|
||||
MODEL_TENSOR.FFN_DOWN,
|
||||
MODEL_TENSOR.FFN_UP,
|
||||
MODEL_TENSOR.INDEXER_Q_PROJ,
|
||||
MODEL_TENSOR.INDEXER_K_PROJ,
|
||||
MODEL_TENSOR.INDEXER_Q_NORM,
|
||||
MODEL_TENSOR.INDEXER_K_NORM,
|
||||
],
|
||||
MODEL_ARCH.COGVLM: [
|
||||
MODEL_TENSOR.TOKEN_EMBD,
|
||||
MODEL_TENSOR.OUTPUT_NORM,
|
||||
@@ -4247,6 +4344,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
|
||||
MODEL_TENSOR.FFN_DOWN,
|
||||
MODEL_TENSOR.FFN_UP,
|
||||
MODEL_TENSOR.FC,
|
||||
MODEL_TENSOR.ENC_OUTPUT_NORM,
|
||||
MODEL_TENSOR.D2T,
|
||||
],
|
||||
MODEL_ARCH.DFLASH: [
|
||||
@@ -4461,7 +4559,22 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
|
||||
MODEL_TENSOR.FFN_DOWN_EXP,
|
||||
MODEL_TENSOR.FFN_UP_EXP,
|
||||
],
|
||||
# TODO
|
||||
MODEL_ARCH.NANBEIGE: [
|
||||
MODEL_TENSOR.TOKEN_EMBD,
|
||||
MODEL_TENSOR.OUTPUT_NORM,
|
||||
MODEL_TENSOR.OUTPUT,
|
||||
MODEL_TENSOR.ROPE_FREQS,
|
||||
MODEL_TENSOR.ATTN_NORM,
|
||||
MODEL_TENSOR.ATTN_Q,
|
||||
MODEL_TENSOR.ATTN_K,
|
||||
MODEL_TENSOR.ATTN_V,
|
||||
MODEL_TENSOR.ATTN_OUT,
|
||||
MODEL_TENSOR.ATTN_ROT_EMBD,
|
||||
MODEL_TENSOR.FFN_NORM,
|
||||
MODEL_TENSOR.FFN_GATE,
|
||||
MODEL_TENSOR.FFN_DOWN,
|
||||
MODEL_TENSOR.FFN_UP,
|
||||
],
|
||||
}
|
||||
|
||||
# tensors that will not be serialized
|
||||
@@ -4528,6 +4641,10 @@ MODEL_TENSOR_SKIP: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
|
||||
MODEL_TENSOR.ROPE_FREQS,
|
||||
MODEL_TENSOR.ATTN_ROT_EMBD,
|
||||
],
|
||||
MODEL_ARCH.NANBEIGE: [
|
||||
MODEL_TENSOR.ROPE_FREQS,
|
||||
MODEL_TENSOR.ATTN_ROT_EMBD,
|
||||
],
|
||||
}
|
||||
|
||||
#
|
||||
@@ -4733,9 +4850,11 @@ class VisionProjectorType:
|
||||
YOUTUVL = "youtuvl"
|
||||
NEMOTRON_V2_VL = "nemotron_v2_vl"
|
||||
HUNYUANVL = "hunyuanvl"
|
||||
MINIMAXM3 = "minimax_m3"
|
||||
MINICPMV4_6 = "minicpmv4_6"
|
||||
GRANITE_SPEECH = "granite_speech" # audio
|
||||
MIMOVL = "mimovl"
|
||||
MIMO_AUDIO = "mimo_audio"
|
||||
GRANITE4_VISION = "granite4_vision"
|
||||
|
||||
|
||||
|
||||
@@ -793,6 +793,12 @@ class GGUFWriter:
|
||||
def add_indexer_top_k(self, top_k: int) -> None:
|
||||
self.add_uint32(Keys.Attention.Indexer.TOP_K.format(arch=self.arch), top_k)
|
||||
|
||||
def add_indexer_block_size(self, block_size: int) -> None:
|
||||
self.add_uint32(Keys.Attention.Indexer.BLOCK_SIZE.format(arch=self.arch), block_size)
|
||||
|
||||
def add_indexer_local_blocks(self, local_blocks: int) -> None:
|
||||
self.add_uint32(Keys.Attention.Indexer.LOCAL_BLOCKS.format(arch=self.arch), local_blocks)
|
||||
|
||||
def add_indexer_types(self, value: Sequence[bool]) -> None:
|
||||
key = Keys.Attention.Indexer.TYPES.format(arch=self.arch)
|
||||
self.add_array(key, value)
|
||||
@@ -902,6 +908,12 @@ class GGUFWriter:
|
||||
def add_token_shift_count(self, count: int) -> None:
|
||||
self.add_uint32(Keys.LLM.TOKEN_SHIFT_COUNT.format(arch=self.arch), count)
|
||||
|
||||
def add_num_loops(self, count: int) -> None:
|
||||
self.add_uint32(Keys.LLM.NUM_LOOPS.format(arch=self.arch), count)
|
||||
|
||||
def add_skip_loop_final_norm(self, value: bool) -> None:
|
||||
self.add_bool(Keys.LLM.SKIP_LOOP_FINAL_NORM.format(arch=self.arch), value)
|
||||
|
||||
def add_interleave_moe_layer_step(self, value: int) -> None:
|
||||
self.add_uint32(Keys.LLM.INTERLEAVE_MOE_LAYER_STEP.format(arch=self.arch), value)
|
||||
|
||||
@@ -959,6 +971,9 @@ class GGUFWriter:
|
||||
def add_norm_before_residual(self, value: bool) -> None:
|
||||
self.add_bool(Keys.LLM.NORM_BEFORE_RESIDUAL.format(arch=self.arch), value)
|
||||
|
||||
def add_norm_before_fc(self, value: bool) -> None:
|
||||
self.add_bool(Keys.LLM.NORM_BEFORE_FC.format(arch=self.arch), value)
|
||||
|
||||
def add_attention_output_group_count(self, count: int) -> None:
|
||||
self.add_uint32(Keys.Attention.OUTPUT_GROUP_COUNT.format(arch=self.arch), count)
|
||||
|
||||
@@ -1338,6 +1353,24 @@ class GGUFWriter:
|
||||
def add_audio_num_mel_bins(self, value: int) -> None:
|
||||
self.add_uint32(Keys.ClipAudio.NUM_MEL_BINS, value)
|
||||
|
||||
def add_audio_rvq_num_quantizers(self, value: int) -> None:
|
||||
self.add_uint32(Keys.ClipAudio.RVQ_NUM_QUANTIZERS, value)
|
||||
|
||||
def add_audio_rvq_codebook_size(self, values: Sequence[int]) -> None:
|
||||
self.add_array(Keys.ClipAudio.RVQ_CODEBOOK_SIZE, values)
|
||||
|
||||
def add_audio_wa_pattern_mode(self, modes: Sequence[int]) -> None:
|
||||
self.add_array(Keys.ClipAudio.WA_PATTERN_MODE, modes)
|
||||
|
||||
def add_audio_window_size(self, value: int) -> None:
|
||||
self.add_uint32(Keys.ClipAudio.WINDOW_SIZE, value)
|
||||
|
||||
def add_audio_local_block_count(self, value: int) -> None:
|
||||
self.add_uint32(Keys.ClipAudio.LOCAL_BLOCK_COUNT, value)
|
||||
|
||||
def add_audio_local_group_size(self, value: int) -> None:
|
||||
self.add_uint32(Keys.ClipAudio.LOCAL_GROUP_SIZE, value)
|
||||
|
||||
def add_audio_stack_factor(self, value: int) -> None:
|
||||
self.add_uint32(Keys.ClipAudio.Projector.STACK_FACTOR, value)
|
||||
|
||||
|
||||
@@ -1264,7 +1264,8 @@ class TensorNameMap:
|
||||
),
|
||||
|
||||
MODEL_TENSOR.INDEXER_K_NORM: (
|
||||
"model.layers.{bid}.self_attn.indexer.k_norm", # DSA
|
||||
"model.layers.{bid}.self_attn.indexer.k_norm", # DSA
|
||||
"model.layers.{bid}.self_attn.index_k_norm", # MSA
|
||||
),
|
||||
|
||||
MODEL_TENSOR.INDEXER_PROJ: (
|
||||
@@ -1279,6 +1280,18 @@ class TensorNameMap:
|
||||
"model.layers.{bid}.self_attn.indexer.wq_b", # DSA
|
||||
),
|
||||
|
||||
MODEL_TENSOR.INDEXER_Q_PROJ: (
|
||||
"model.layers.{bid}.self_attn.index_q_proj", # MSA
|
||||
),
|
||||
|
||||
MODEL_TENSOR.INDEXER_K_PROJ: (
|
||||
"model.layers.{bid}.self_attn.index_k_proj", # MSA
|
||||
),
|
||||
|
||||
MODEL_TENSOR.INDEXER_Q_NORM: (
|
||||
"model.layers.{bid}.self_attn.index_q_norm", # MSA
|
||||
),
|
||||
|
||||
############################################################################
|
||||
# TODO: these do not belong to block_mappings_cfg - move them to mappings_cfg
|
||||
MODEL_TENSOR.ENC_OUTPUT_NORM: (
|
||||
@@ -1825,6 +1838,14 @@ class TensorNameMap:
|
||||
"visual.downsample", # glm4v
|
||||
),
|
||||
|
||||
MODEL_TENSOR.V_MM_MERGER_FC1: (
|
||||
"patch_merge_mlp.linear_1", # minimax-m3
|
||||
),
|
||||
|
||||
MODEL_TENSOR.V_MM_MERGER_FC2: (
|
||||
"patch_merge_mlp.linear_2", # minimax-m3
|
||||
),
|
||||
|
||||
MODEL_TENSOR.V_DS_NORM: (
|
||||
"model.visual.deepstack_merger_list.{bid}.norm", # deepstack in qwen3vl
|
||||
),
|
||||
@@ -2074,6 +2095,7 @@ class TensorNameMap:
|
||||
"conformer.pre_encode.conv.{bid}", # lfm2
|
||||
"model.audio_tower.subsample_conv_projection.conv_{bid}.conv", # gemma3n
|
||||
"conformer.subsample_conv_projection.layer{bid}.conv", # gemma4
|
||||
"encoder.conv{bid}", # mimo-audio-tokenizer
|
||||
),
|
||||
|
||||
MODEL_TENSOR.A_ENC_CONV1D_NORM: (
|
||||
@@ -2098,6 +2120,7 @@ class TensorNameMap:
|
||||
MODEL_TENSOR.A_POST_NORM: (
|
||||
"audio_tower.layer_norm", # ultravox
|
||||
"audio_tower.ln_post", # qwen2omni
|
||||
"encoder.layer_norm", # mimo-audio-tokenizer
|
||||
),
|
||||
|
||||
MODEL_TENSOR.A_ENC_ATTN_Q: (
|
||||
@@ -2106,6 +2129,7 @@ class TensorNameMap:
|
||||
"conformer.layers.{bid}.attention.attn.q_proj", # gemma3n
|
||||
"conformer.layers.{bid}.self_attn.q_proj", # gemma4
|
||||
"encoder.layers.{bid}.attn.to_q", # granite_speech
|
||||
"encoder.layers.{bid}.self_attn.q_proj", # mimo-audio-tokenizer
|
||||
),
|
||||
|
||||
MODEL_TENSOR.A_ENC_ATTN_K: (
|
||||
@@ -2114,6 +2138,7 @@ class TensorNameMap:
|
||||
"conformer.layers.{bid}.attention.attn.k_proj", # gemma3n
|
||||
"conformer.layers.{bid}.self_attn.k_proj", # gemma4
|
||||
"encoder.layers.{bid}.attn.to_k", # granite_speech (split from to_kv)
|
||||
"encoder.layers.{bid}.self_attn.k_proj", # mimo-audio-tokenizer
|
||||
),
|
||||
|
||||
MODEL_TENSOR.A_ENC_ATTN_V: (
|
||||
@@ -2122,6 +2147,7 @@ class TensorNameMap:
|
||||
"conformer.layers.{bid}.attention.attn.v_proj", # gemma3n
|
||||
"conformer.layers.{bid}.self_attn.v_proj", # gemma4
|
||||
"encoder.layers.{bid}.attn.to_v", # granite_speech (split from to_kv)
|
||||
"encoder.layers.{bid}.self_attn.v_proj", # mimo-audio-tokenizer
|
||||
),
|
||||
|
||||
MODEL_TENSOR.A_ENC_ATTN_K_REL: (
|
||||
@@ -2150,6 +2176,7 @@ class TensorNameMap:
|
||||
"conformer.layers.{bid}.norm_self_att", # lfm2
|
||||
"conformer.layers.{bid}.attention.pre_attn_norm", # gemma3n
|
||||
"encoder.layers.{bid}.attn.pre_norm", # granite_speech
|
||||
"encoder.layers.{bid}.self_attn_layer_norm", # mimo-audio-tokenizer
|
||||
),
|
||||
|
||||
MODEL_TENSOR.A_ENC_OUTPUT: (
|
||||
@@ -2158,6 +2185,7 @@ class TensorNameMap:
|
||||
"conformer.layers.{bid}.attention.post", # gemma3n
|
||||
"conformer.layers.{bid}.self_attn.post", # gemma4
|
||||
"encoder.layers.{bid}.attn.to_out", # granite_speech
|
||||
"encoder.layers.{bid}.self_attn.out_proj", # mimo-audio-tokenizer
|
||||
),
|
||||
|
||||
MODEL_TENSOR.A_ENC_OUTPUT_NORM: (
|
||||
@@ -2165,6 +2193,7 @@ class TensorNameMap:
|
||||
"conformer.layers.{bid}.norm_out", # lfm2
|
||||
"conformer.layers.{bid}.attention.post_norm", # gemma3n
|
||||
"encoder.layers.{bid}.post_norm", # granite_speech
|
||||
"encoder.layers.{bid}.final_layer_norm", # mimo-audio-tokenizer
|
||||
),
|
||||
|
||||
MODEL_TENSOR.A_ENC_FFN_NORM: (
|
||||
@@ -2189,6 +2218,7 @@ class TensorNameMap:
|
||||
"conformer.layers.{bid}.ffw_layer_start.ffw_layer_1", # gemma3n
|
||||
"conformer.layers.{bid}.feed_forward1.ffw_layer_1", # gemma4
|
||||
"encoder.layers.{bid}.ff1.up_proj", # granite_speech
|
||||
"encoder.layers.{bid}.fc1", # mimo-audio-tokenizer
|
||||
),
|
||||
|
||||
MODEL_TENSOR.A_ENC_FFN_GATE: (),
|
||||
@@ -2199,6 +2229,7 @@ class TensorNameMap:
|
||||
"conformer.layers.{bid}.ffw_layer_start.ffw_layer_2", # gemma3n
|
||||
"conformer.layers.{bid}.feed_forward1.ffw_layer_2", # gemma4
|
||||
"encoder.layers.{bid}.ff1.down_proj", # granite_speech
|
||||
"encoder.layers.{bid}.fc2", # mimo-audio-tokenizer
|
||||
),
|
||||
|
||||
MODEL_TENSOR.A_ENC_FFN_UP_1: (
|
||||
@@ -2222,6 +2253,19 @@ class TensorNameMap:
|
||||
"encoder.layers.{bid}.ff2.pre_norm", # granite_speech
|
||||
),
|
||||
|
||||
MODEL_TENSOR.A_ENC_DOWNSAMPLE_CONV: (
|
||||
"encoder.down_sample_layer.0", # mimo-audio-tokenizer
|
||||
),
|
||||
|
||||
MODEL_TENSOR.A_ENC_DOWNSAMPLE_NORM: (
|
||||
"encoder.down_sample_norm", # mimo-audio-tokenizer
|
||||
),
|
||||
|
||||
# note: the raw per-quantizer "encoder.quantizer.vq.layers.{i}._codebook.embed"
|
||||
# tensors are merged (padded + stacked, like MoE experts) into this single 3D
|
||||
# tensor in conversion code, so no raw-name mapping is registered here.
|
||||
MODEL_TENSOR.A_ENC_RVQ_CODEBOOK: (),
|
||||
|
||||
MODEL_TENSOR.A_ENC_FFN_POST_NORM_1: (
|
||||
"conformer.layers.{bid}.ffw_layer_end.post_layer_norm", # gemma3n
|
||||
"conformer.layers.{bid}.feed_forward2.post_layer_norm", # gemma4
|
||||
@@ -2273,6 +2317,42 @@ class TensorNameMap:
|
||||
"audio.multi_modal_projector.ln_mid", # ultravox
|
||||
),
|
||||
|
||||
# note: the raw per-channel "speech_embeddings.{i}" tensors are merged
|
||||
# (stacked, like MoE experts) into this single 3D tensor in conversion
|
||||
# code, so no raw-name mapping is registered here.
|
||||
MODEL_TENSOR.A_MM_CODE_EMBD: (),
|
||||
|
||||
MODEL_TENSOR.A_MM_LOCAL_ATTN_Q: (
|
||||
"audio_encoder.input_local_transformer.layers.{bid}.self_attn.q_proj", # mimo-v2.5
|
||||
),
|
||||
MODEL_TENSOR.A_MM_LOCAL_ATTN_K: (
|
||||
"audio_encoder.input_local_transformer.layers.{bid}.self_attn.k_proj", # mimo-v2.5
|
||||
),
|
||||
MODEL_TENSOR.A_MM_LOCAL_ATTN_V: (
|
||||
"audio_encoder.input_local_transformer.layers.{bid}.self_attn.v_proj", # mimo-v2.5
|
||||
),
|
||||
MODEL_TENSOR.A_MM_LOCAL_ATTN_OUT: (
|
||||
"audio_encoder.input_local_transformer.layers.{bid}.self_attn.o_proj", # mimo-v2.5
|
||||
),
|
||||
MODEL_TENSOR.A_MM_LOCAL_FFN_GATE: (
|
||||
"audio_encoder.input_local_transformer.layers.{bid}.mlp.gate_proj", # mimo-v2.5
|
||||
),
|
||||
MODEL_TENSOR.A_MM_LOCAL_FFN_UP: (
|
||||
"audio_encoder.input_local_transformer.layers.{bid}.mlp.up_proj", # mimo-v2.5
|
||||
),
|
||||
MODEL_TENSOR.A_MM_LOCAL_FFN_DOWN: (
|
||||
"audio_encoder.input_local_transformer.layers.{bid}.mlp.down_proj", # mimo-v2.5
|
||||
),
|
||||
MODEL_TENSOR.A_MM_LOCAL_LN1: (
|
||||
"audio_encoder.input_local_transformer.layers.{bid}.input_layernorm", # mimo-v2.5
|
||||
),
|
||||
MODEL_TENSOR.A_MM_LOCAL_LN2: (
|
||||
"audio_encoder.input_local_transformer.layers.{bid}.post_attention_layernorm", # mimo-v2.5
|
||||
),
|
||||
MODEL_TENSOR.A_MM_LOCAL_NORM: (
|
||||
"audio_encoder.input_local_transformer.norm", # mimo-v2.5
|
||||
),
|
||||
|
||||
MODEL_TENSOR.A_ENC_CONV_DW: (
|
||||
"conformer.layers.{bid}.conv.depthwise_conv", # lfm2
|
||||
"conformer.layers.{bid}.lconv1d.depthwise_conv1d", # gemma3n
|
||||
|
||||
+5
-4
@@ -203,10 +203,11 @@ extern "C" {
|
||||
};
|
||||
|
||||
enum llama_load_mode {
|
||||
LLAMA_LOAD_MODE_NONE = 0, // no special loading mode
|
||||
LLAMA_LOAD_MODE_MMAP = 1, // memory map the model
|
||||
LLAMA_LOAD_MODE_MLOCK = 2, // mmap + force system to keep model in RAM rather than swapping or compressing
|
||||
LLAMA_LOAD_MODE_DIRECT_IO = 3, // use direct I/O if available
|
||||
LLAMA_LOAD_MODE_NONE = 0, // no special loading mode
|
||||
LLAMA_LOAD_MODE_MMAP = 1, // memory map the model
|
||||
LLAMA_LOAD_MODE_MLOCK = 2, // force system to keep model in RAM rather than swapping or compressing
|
||||
LLAMA_LOAD_MODE_MMAP_MLOCK = 3, // mmap + force system to keep model in RAM rather than swapping or compressing
|
||||
LLAMA_LOAD_MODE_DIRECT_IO = 4, // use direct I/O if available
|
||||
};
|
||||
|
||||
LLAMA_API const char * llama_load_mode_name(enum llama_load_mode load_mode);
|
||||
|
||||
@@ -76,6 +76,7 @@ These recur often enough in review comments on past add-model PRs that they're w
|
||||
- Don't ship unfinished or unverified speculative-decoding (e.g. MTP) scaffolding in the base model PR - if it hasn't actually been confirmed to work, pull it out and land it as its own follow-up.
|
||||
- Conversion code should call into the base class's existing hparam logic (e.g. `super().set_gguf_parameters()`) rather than re-deriving it - large blocks of code that duplicate what `TextModel`/`MmprojModel` already provide will get flagged as redundant.
|
||||
- Do constant tensor modifications (e.g. `norm(1 + weight)`) and permutations/chunking at conversion time, not in the graph - see HOWTO-add-model.md's "Prefer conversion-time tensor modifications" tip (Gemma 3 folds its `1 +` into the weights, Qwen3-Next permutes in `modify_tensors`). Doing these at runtime in the graph is very likely to be rejected as over-complicated; if you genuinely can't do it at conversion time, open a discussion first explaining why rather than implementing it in the graph.
|
||||
- Exception: a plain `weight * scale` with a constant scale is usually better applied at inference time instead of being folded into the weight at conversion. The scale conceptually applies to the activation, not the weight, so folding it in can hurt numerical stability, and it shifts the weight's value range in a way that can make quantization worse.
|
||||
|
||||
## Validation checklist
|
||||
|
||||
|
||||
@@ -110,6 +110,15 @@ Public API changes carry a higher bar than internal ones (`CONTRIBUTING.md`). Re
|
||||
- Security: don't trust client-supplied headers (e.g. `X-Forwarded-For`) or add footguns; things like IP allowlisting belong at a reverse proxy unless there's a trusted-proxy design.
|
||||
- Wire new behavior into the existing request/response and checkpoint paths correctly; watch for resource leaks across requests.
|
||||
|
||||
## Multimodal (`tools/mtmd/`)
|
||||
|
||||
- Tensor names must be prefixed by `v.`, `a.`, `mm.` or `a.mm.` (legacy naming doesn't follow this convention - this is expected, but new code should follow it).
|
||||
- Do not use explicit sin/cos for RoPE; use `ggml_rope_ext` instead, see `HOWTO-add-model.md`. If it can't express the needed behavior, that's a design discussion, not a PR.
|
||||
- New GGML ops must not be introduced in the same PR, you must push it as a separate PR.
|
||||
- In most cases, `build_vit` should be enough to build the transformer graph for vision models. Do not add a loop to build the transformer graph manually, unless you have a very good reason to do so. If you do, please explain why in the PR description.
|
||||
- If you need a dedicated preprocessor, there is a high chance that it can be a derived class from one of the existing preprocessors. Check carefully before adding a new preprocessor class.
|
||||
- If the model need a new public API in `mtmd.h`, open a discussion first.
|
||||
|
||||
## General (always)
|
||||
|
||||
Enforce the `AGENTS.md` / `CONTRIBUTING.md` coding and naming guidelines on every changed line - this is a distinct pass from checking that the code works, and matters just as much for review speed:
|
||||
|
||||
@@ -127,6 +127,7 @@ static const std::map<llm_arch, const char *> LLM_ARCH_NAMES = {
|
||||
{ LLM_ARCH_GROVEMOE, "grovemoe" },
|
||||
{ LLM_ARCH_APERTUS, "apertus" },
|
||||
{ LLM_ARCH_MINIMAX_M2, "minimax-m2" },
|
||||
{ LLM_ARCH_MINIMAX_M3, "minimax-m3" },
|
||||
{ LLM_ARCH_COGVLM, "cogvlm" },
|
||||
{ LLM_ARCH_RND1, "rnd1" },
|
||||
{ LLM_ARCH_PANGU_EMBED, "pangu-embedded" },
|
||||
@@ -142,6 +143,7 @@ static const std::map<llm_arch, const char *> LLM_ARCH_NAMES = {
|
||||
{ LLM_ARCH_KIMI_LINEAR, "kimi-linear" },
|
||||
{ LLM_ARCH_TALKIE, "talkie" },
|
||||
{ LLM_ARCH_MELLUM, "mellum" },
|
||||
{ LLM_ARCH_NANBEIGE, "nanbeige" },
|
||||
{ LLM_ARCH_UNKNOWN, "(unknown)" },
|
||||
};
|
||||
|
||||
@@ -220,6 +222,8 @@ static const std::map<llm_kv, const char *> LLM_KV_NAMES = {
|
||||
{ LLM_KV_TOKEN_SHIFT_COUNT, "%s.token_shift_count" },
|
||||
{ LLM_KV_INTERLEAVE_MOE_LAYER_STEP, "%s.interleave_moe_layer_step" },
|
||||
{ LLM_KV_FULL_ATTENTION_INTERVAL, "%s.full_attention_interval" },
|
||||
{ LLM_KV_NUM_LOOPS, "%s.num_loops" },
|
||||
{ LLM_KV_SKIP_LOOP_FINAL_NORM, "%s.skip_loop_final_norm" },
|
||||
|
||||
{ LLM_KV_ATTENTION_HEAD_COUNT, "%s.attention.head_count" },
|
||||
{ LLM_KV_ATTENTION_HEAD_COUNT_KV, "%s.attention.head_count_kv" },
|
||||
@@ -253,6 +257,8 @@ static const std::map<llm_kv, const char *> LLM_KV_NAMES = {
|
||||
{ LLM_KV_ATTENTION_INDEXER_HEAD_COUNT, "%s.attention.indexer.head_count" },
|
||||
{ LLM_KV_ATTENTION_INDEXER_KEY_LENGTH, "%s.attention.indexer.key_length" },
|
||||
{ LLM_KV_ATTENTION_INDEXER_TOP_K, "%s.attention.indexer.top_k" },
|
||||
{ LLM_KV_ATTENTION_INDEXER_BLOCK_SIZE, "%s.attention.indexer.block_size" },
|
||||
{ LLM_KV_ATTENTION_INDEXER_LOCAL_BLOCKS, "%s.attention.indexer.local_blocks" },
|
||||
{ LLM_KV_ATTENTION_INDEXER_TYPES, "%s.attention.indexer.types" },
|
||||
{ LLM_KV_ATTENTION_OUTPUT_GROUP_COUNT, "%s.attention.output_group_count" },
|
||||
{ LLM_KV_ATTENTION_OUTPUT_LORA_RANK, "%s.attention.output_lora_rank" },
|
||||
@@ -311,6 +317,7 @@ static const std::map<llm_kv, const char *> LLM_KV_NAMES = {
|
||||
{ LLM_KV_TARGET_LAYERS, "%s.target_layers" },
|
||||
{ LLM_KV_TARGET_HIDDEN_SIZE, "%s.target_hidden_size" },
|
||||
{ LLM_KV_NORM_BEFORE_RESIDUAL, "%s.norm_before_residual" },
|
||||
{ LLM_KV_NORM_BEFORE_FC, "%s.norm_before_fc" },
|
||||
|
||||
{ LLM_KV_SHORTCONV_L_CACHE, "%s.shortconv.l_cache" },
|
||||
// sentence-transformers dense modules feature dims
|
||||
@@ -597,6 +604,9 @@ static const std::map<llm_tensor, const char *> LLM_TENSOR_NAMES = {
|
||||
{ LLM_TENSOR_INDEXER_PROJ, "blk.%d.indexer.proj" },
|
||||
{ LLM_TENSOR_INDEXER_ATTN_K, "blk.%d.indexer.attn_k" },
|
||||
{ LLM_TENSOR_INDEXER_ATTN_Q_B, "blk.%d.indexer.attn_q_b" },
|
||||
{ LLM_TENSOR_INDEXER_Q_PROJ, "blk.%d.indexer.q_proj" },
|
||||
{ LLM_TENSOR_INDEXER_K_PROJ, "blk.%d.indexer.k_proj" },
|
||||
{ LLM_TENSOR_INDEXER_Q_NORM, "blk.%d.indexer.q_norm" },
|
||||
{ LLM_TENSOR_INDEXER_COMPRESSOR_WKV, "blk.%d.indexer_compressor_kv" },
|
||||
{ LLM_TENSOR_INDEXER_COMPRESSOR_WGATE, "blk.%d.indexer_compressor_gate" },
|
||||
{ LLM_TENSOR_INDEXER_COMPRESSOR_APE, "blk.%d.indexer_compressor_ape" },
|
||||
@@ -832,6 +842,9 @@ static const std::map<llm_tensor, llm_tensor_info> LLM_TENSOR_INFOS = {
|
||||
{LLM_TENSOR_INDEXER_PROJ, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
|
||||
{LLM_TENSOR_INDEXER_ATTN_K, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
|
||||
{LLM_TENSOR_INDEXER_ATTN_Q_B, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
|
||||
{LLM_TENSOR_INDEXER_Q_PROJ, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
|
||||
{LLM_TENSOR_INDEXER_K_PROJ, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
|
||||
{LLM_TENSOR_INDEXER_Q_NORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
|
||||
{LLM_TENSOR_INDEXER_COMPRESSOR_WKV, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
|
||||
{LLM_TENSOR_INDEXER_COMPRESSOR_WGATE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
|
||||
{LLM_TENSOR_INDEXER_COMPRESSOR_APE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_GET_ROWS}},
|
||||
@@ -1001,6 +1014,7 @@ bool llm_arch_supports_sm_tensor(const llm_arch & arch) {
|
||||
case LLM_ARCH_LFM2:
|
||||
case LLM_ARCH_LFM2MOE:
|
||||
case LLM_ARCH_MINIMAX_M2:
|
||||
case LLM_ARCH_MINIMAX_M3:
|
||||
case LLM_ARCH_MISTRAL4:
|
||||
case LLM_ARCH_KIMI_LINEAR:
|
||||
return false;
|
||||
|
||||
@@ -146,7 +146,9 @@ enum llm_arch {
|
||||
LLM_ARCH_TALKIE,
|
||||
LLM_ARCH_MELLUM,
|
||||
LLM_ARCH_EAGLE3,
|
||||
LLM_ARCH_MINIMAX_M3,
|
||||
LLM_ARCH_DFLASH,
|
||||
LLM_ARCH_NANBEIGE,
|
||||
LLM_ARCH_UNKNOWN,
|
||||
};
|
||||
|
||||
@@ -225,6 +227,8 @@ enum llm_kv {
|
||||
LLM_KV_TOKEN_SHIFT_COUNT,
|
||||
LLM_KV_INTERLEAVE_MOE_LAYER_STEP,
|
||||
LLM_KV_FULL_ATTENTION_INTERVAL,
|
||||
LLM_KV_NUM_LOOPS,
|
||||
LLM_KV_SKIP_LOOP_FINAL_NORM,
|
||||
|
||||
LLM_KV_ATTENTION_HEAD_COUNT,
|
||||
LLM_KV_ATTENTION_HEAD_COUNT_KV,
|
||||
@@ -258,6 +262,8 @@ enum llm_kv {
|
||||
LLM_KV_ATTENTION_INDEXER_HEAD_COUNT,
|
||||
LLM_KV_ATTENTION_INDEXER_KEY_LENGTH,
|
||||
LLM_KV_ATTENTION_INDEXER_TOP_K,
|
||||
LLM_KV_ATTENTION_INDEXER_BLOCK_SIZE,
|
||||
LLM_KV_ATTENTION_INDEXER_LOCAL_BLOCKS,
|
||||
LLM_KV_ATTENTION_INDEXER_TYPES,
|
||||
LLM_KV_ATTENTION_OUTPUT_GROUP_COUNT,
|
||||
LLM_KV_ATTENTION_OUTPUT_LORA_RANK,
|
||||
@@ -357,6 +363,7 @@ enum llm_kv {
|
||||
LLM_KV_TARGET_LAYERS,
|
||||
LLM_KV_TARGET_HIDDEN_SIZE,
|
||||
LLM_KV_NORM_BEFORE_RESIDUAL,
|
||||
LLM_KV_NORM_BEFORE_FC,
|
||||
|
||||
LLM_KV_SHORTCONV_L_CACHE,
|
||||
|
||||
@@ -597,6 +604,9 @@ enum llm_tensor {
|
||||
LLM_TENSOR_INDEXER_PROJ,
|
||||
LLM_TENSOR_INDEXER_ATTN_K,
|
||||
LLM_TENSOR_INDEXER_ATTN_Q_B,
|
||||
LLM_TENSOR_INDEXER_Q_PROJ,
|
||||
LLM_TENSOR_INDEXER_K_PROJ,
|
||||
LLM_TENSOR_INDEXER_Q_NORM,
|
||||
LLM_TENSOR_INDEXER_COMPRESSOR_WKV,
|
||||
LLM_TENSOR_INDEXER_COMPRESSOR_WGATE,
|
||||
LLM_TENSOR_INDEXER_COMPRESSOR_APE,
|
||||
|
||||
@@ -2338,7 +2338,9 @@ uint32_t llama_context::graph_max_nodes(uint32_t n_tokens) const {
|
||||
model.arch == LLM_ARCH_KIMI_LINEAR ||
|
||||
model.arch == LLM_ARCH_QWEN35 ||
|
||||
model.arch == LLM_ARCH_QWEN35MOE ||
|
||||
model.arch == LLM_ARCH_DEEPSEEK4) {
|
||||
model.arch == LLM_ARCH_DEEPSEEK4 ||
|
||||
model.arch == LLM_ARCH_NANBEIGE ||
|
||||
model.arch == LLM_ARCH_MINIMAX_M3) {
|
||||
return std::max<uint32_t>(n_tokens * 40, 32u * model.n_tensors());
|
||||
}
|
||||
uint32_t res = std::max<uint32_t>(1024u, 8u*model.n_tensors());
|
||||
@@ -2472,11 +2474,12 @@ llm_graph_cb llama_context::graph_get_cb() const {
|
||||
ggml_set_name(cur, name);
|
||||
}
|
||||
|
||||
// norm may be automatically assigned to the backend of the previous layer, increasing data transfer between backends
|
||||
// - norm may be automatically assigned to the backend of the previous layer, increasing data transfer between backends
|
||||
// - force the last op of the layer on the specified backend to avoid running it on the backend of the next layer due to scheduling
|
||||
// FIXME: fix in ggml_backend_sched
|
||||
const bool full_offload = model.n_gpu_layers() > model.hparams.n_layer_all;
|
||||
if (ubatch.n_tokens < 32 || full_offload) {
|
||||
if (il != -1 && strcmp(name, "norm") == 0) {
|
||||
if (il != -1 && (strcmp(name, "norm") == 0 || strcmp(name, "l_last") == 0)) {
|
||||
const auto & dev_layer = model.dev_layer(il);
|
||||
for (const auto & backend : backends) {
|
||||
if (ggml_backend_get_device(backend.get()) == dev_layer) {
|
||||
|
||||
+12
-1
@@ -1709,6 +1709,17 @@ ggml_tensor * llm_graph_context::build_ffn(
|
||||
cur = ggml_swiglu(ctx0, cur);
|
||||
cb(cur, "ffn_swiglu", il);
|
||||
} break;
|
||||
case LLM_FFN_SWIGLU_OAI_MOE:
|
||||
if (gate && type_gate == LLM_FFN_PAR) {
|
||||
// same alpha/limit constants as gpt-oss
|
||||
const float alpha = 1.702f;
|
||||
const float limit = 7.0f;
|
||||
cur = ggml_swiglu_oai(ctx0, cur, tmp, alpha, limit);
|
||||
cb(cur, "ffn_swiglu_oai", il);
|
||||
type_gate = LLM_FFN_SEQ;
|
||||
} else {
|
||||
GGML_ABORT("LLM_FFN_SWIGLU_OAI_MOE requires a parallel gate");
|
||||
} break;
|
||||
case LLM_FFN_GEGLU:
|
||||
{
|
||||
cur = ggml_geglu(ctx0, cur);
|
||||
@@ -2668,7 +2679,7 @@ ggml_tensor * llm_graph_context::build_attn(
|
||||
ggml_build_forward_expand(gf, mctx_cur->cpy_v(ctx0, v_cur, v_idxs, il));
|
||||
}
|
||||
|
||||
const auto & kq_mask = inp->get_kq_mask();
|
||||
ggml_tensor * kq_mask = inp->get_kq_mask();
|
||||
|
||||
ggml_tensor * q = q_cur;
|
||||
ggml_tensor * k = mctx_cur->get_k(ctx0, il);
|
||||
|
||||
@@ -180,6 +180,16 @@ uint32_t llama_hparams::n_embd_v_gqa_max() const {
|
||||
return val;
|
||||
}
|
||||
|
||||
uint32_t llama_hparams::n_embd_k_idx(uint32_t il) const {
|
||||
if (!indexer_kv || indexer_head_size == 0) {
|
||||
return 0; // arch without a MSA indexer
|
||||
}
|
||||
if (il < n_layer_dense_lead) {
|
||||
return 0; // leading dense layers carry no indexer
|
||||
}
|
||||
return indexer_head_size; // 128
|
||||
}
|
||||
|
||||
uint32_t llama_hparams::n_embd_r() const {
|
||||
if (wkv_head_size != 0) {
|
||||
// for RWKV models
|
||||
|
||||
@@ -47,6 +47,7 @@ struct llama_hparams {
|
||||
bool use_par_res;
|
||||
bool swin_norm;
|
||||
bool norm_before_residual = false;
|
||||
bool norm_before_fc = false;
|
||||
|
||||
uint32_t n_ctx_train; // context size the model was trained on
|
||||
uint32_t n_embd;
|
||||
@@ -226,6 +227,11 @@ struct llama_hparams {
|
||||
uint32_t indexer_n_head = 0;
|
||||
uint32_t indexer_head_size = 0;
|
||||
uint32_t indexer_top_k = 0;
|
||||
// MSA
|
||||
uint32_t indexer_block_size = 0;
|
||||
uint32_t indexer_local_blocks = 0;
|
||||
// MSA stores its indexer keys in the main KV cache (k_idx tensors);
|
||||
bool indexer_kv = false;
|
||||
|
||||
// Indexer is "full" (1) or "shared" (0)
|
||||
// Shared indexers reuse top-k from previous full layer
|
||||
@@ -350,6 +356,9 @@ struct llama_hparams {
|
||||
uint32_t n_embd_k_gqa_max() const;
|
||||
uint32_t n_embd_v_gqa_max() const;
|
||||
|
||||
// dimension of the single-head MSA indexer key stream
|
||||
uint32_t n_embd_k_idx(uint32_t il = 0) const;
|
||||
|
||||
// dimension of the rolling state embeddings
|
||||
// corresponds to Mamba's conv_states size or RWKV's token_shift states size
|
||||
uint32_t n_embd_r() const;
|
||||
|
||||
+278
-14
@@ -112,7 +112,7 @@ llama_kv_cache::llama_kv_cache(
|
||||
auto it = ctx_map.find(buft);
|
||||
if (it == ctx_map.end()) {
|
||||
ggml_init_params params = {
|
||||
/*.mem_size =*/ size_t(2u*(1 + n_stream)*n_layer*ggml_tensor_overhead()),
|
||||
/*.mem_size =*/ size_t(3u*(1 + n_stream)*n_layer*ggml_tensor_overhead()), //Reserve tensor metadata for up to 3 tensors per layer (K, V, and optional K_idx), plus one view per tensor per stream.
|
||||
/*.mem_buffer =*/ NULL,
|
||||
/*.no_alloc =*/ true,
|
||||
};
|
||||
@@ -242,9 +242,25 @@ llama_kv_cache::llama_kv_cache(
|
||||
v_stream.push_back(has_v ? ggml_view_2d(ctx, v, n_embd_v_gqa, kv_size, v->nb[1], s*v->nb[2]) : nullptr);
|
||||
}
|
||||
|
||||
const uint32_t n_embd_k_idx = hparams.n_embd_k_idx(il);
|
||||
ggml_tensor * k_idx = n_embd_k_idx > 0
|
||||
? ggml_new_tensor_3d(ctx, GGML_TYPE_F32, n_embd_k_idx, kv_size, n_stream)
|
||||
: nullptr;
|
||||
if (k_idx) {
|
||||
ggml_format_name(k_idx, "cache_k_idx_l%d", il);
|
||||
msa_strict_slots = (n_stream == n_seq_max);
|
||||
}
|
||||
|
||||
std::vector<ggml_tensor *> k_idx_stream;
|
||||
for (uint32_t s = 0; s < n_stream; ++s) {
|
||||
k_idx_stream.push_back(k_idx
|
||||
? ggml_view_2d(ctx, k_idx, n_embd_k_idx, kv_size, k_idx->nb[1], s*k_idx->nb[2])
|
||||
: nullptr);
|
||||
}
|
||||
|
||||
map_layer_ids[il] = layers.size();
|
||||
|
||||
layers.push_back({ il, k, v, k_stream, v_stream, });
|
||||
layers.push_back({ il, k, v, k_idx, k_stream, v_stream, k_idx_stream });
|
||||
}
|
||||
|
||||
if (reuse) {
|
||||
@@ -293,13 +309,24 @@ llama_kv_cache::llama_kv_cache(
|
||||
}
|
||||
|
||||
{
|
||||
const size_t memory_size_k = size_k_bytes();
|
||||
const size_t memory_size_v = size_v_bytes();
|
||||
const size_t memory_size_k = size_k_bytes();
|
||||
const size_t memory_size_v = size_v_bytes();
|
||||
const size_t memory_size_k_idx = size_k_idx_bytes();
|
||||
const size_t memory_size_total = memory_size_k + memory_size_v + memory_size_k_idx;
|
||||
|
||||
LLAMA_LOG_INFO("%s: size = %7.2f MiB (%6u cells, %3d layers, %2u/%u seqs), K (%s): %7.2f MiB, V (%s): %7.2f MiB\n", __func__,
|
||||
(float)(memory_size_k + memory_size_v) / (1024.0f * 1024.0f), kv_size, (int) layers.size(), n_seq_max, n_stream,
|
||||
ggml_type_name(type_k), (float)memory_size_k / (1024.0f * 1024.0f),
|
||||
ggml_type_name(type_v), (float)memory_size_v / (1024.0f * 1024.0f));
|
||||
constexpr float mib = 1024.0f * 1024.0f;
|
||||
|
||||
const std::string k_log = format(", K (%s): %7.2f MiB", ggml_type_name(type_k), (float) memory_size_k / mib);
|
||||
const std::string v_log = format(", V (%s): %7.2f MiB", ggml_type_name(type_v), (float) memory_size_v / mib);
|
||||
|
||||
std::string k_idx_log;
|
||||
if (memory_size_k_idx > 0) {
|
||||
k_idx_log = format(", K_idx (%s): %7.2f MiB", ggml_type_name(GGML_TYPE_F32), (float) memory_size_k_idx / mib);
|
||||
}
|
||||
|
||||
LLAMA_LOG_INFO("%s: size = %7.2f MiB (%6u cells, %3d layers, %2u/%u seqs)%s%s%s\n", __func__,
|
||||
(float) memory_size_total / mib, kv_size, (int) layers.size(), n_seq_max, n_stream,
|
||||
k_log.c_str(), v_log.c_str(), k_idx_log.c_str());
|
||||
}
|
||||
|
||||
// TODO: refactor [TAG_KV_CACHE_SHARE_CELLS]
|
||||
@@ -392,6 +419,39 @@ bool llama_kv_cache::seq_rm(llama_seq_id seq_id, llama_pos p0, llama_pos p1) {
|
||||
p1 = std::numeric_limits<llama_pos>::max();
|
||||
}
|
||||
|
||||
// empty range - nothing to remove
|
||||
if (p0 >= p1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// MSA anchors block selection to absolute cache slots (slot == position). Tail trim and full removal preserve this invariant, but removing a prefix
|
||||
// or middle range would free slots while later cells survive, desynchronizing the indexer cache. Reject such removals before modifying the cache.
|
||||
if (msa_strict_slots) {
|
||||
for (llama_seq_id sid = 0; sid < (llama_seq_id) seq_to_stream.size(); ++sid) {
|
||||
if (seq_id >= 0 && sid != seq_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto & cells = v_cells[seq_to_stream[sid]];
|
||||
|
||||
const llama_pos pmin = cells.seq_pos_min(sid);
|
||||
const llama_pos pmax = cells.seq_pos_max(sid);
|
||||
|
||||
if (pmin < 0) {
|
||||
continue; // empty sequence
|
||||
}
|
||||
|
||||
const bool overlaps = p0 <= pmax && p1 > pmin; // the range removes something
|
||||
const bool leaves_tail = p1 <= pmax; // cells beyond the range survive
|
||||
|
||||
if (overlaps && leaves_tail) {
|
||||
LLAMA_LOG_WARN("%s: MSA: partial (non-suffix) removal [%d, %d) for seq %d is not supported "
|
||||
"(block selection is anchored to cache slots) - rejected\n", __func__, p0, p1, sid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (seq_id >= 0) {
|
||||
auto & cells = v_cells[seq_to_stream[seq_id]];
|
||||
auto & head = v_heads[seq_to_stream[seq_id]];
|
||||
@@ -846,6 +906,10 @@ bool llama_kv_cache::update(llama_context * lctx, bool do_shift, const stream_co
|
||||
if (layer.v_stream[ssrc]) {
|
||||
ggml_backend_tensor_copy(layer.v_stream[ssrc], layer.v_stream[sdst]);
|
||||
}
|
||||
if (layer.k_idx_stream[ssrc]) {
|
||||
GGML_ASSERT(layer.k_idx_stream[sdst]);
|
||||
ggml_backend_tensor_copy(layer.k_idx_stream[ssrc], layer.k_idx_stream[sdst]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -994,6 +1058,44 @@ llama_kv_cache::slot_info llama_kv_cache::find_slot(const llama_ubatch & ubatch,
|
||||
|
||||
const auto & cells = v_cells[seq_to_stream[seq_id]];
|
||||
|
||||
if (n_tokens > cells.size()) {
|
||||
LLAMA_LOG_ERROR("%s: n_tokens = %d > size = %u\n", __func__, n_tokens, cells.size());
|
||||
return { };
|
||||
}
|
||||
|
||||
// MSA block selection assumes slot == logical position (append-only streams).
|
||||
if (msa_strict_slots) {
|
||||
for (uint32_t ii = 0; ii < n_tokens; ++ii) {
|
||||
const llama_pos pos = ubatch.pos[s*n_tokens + ii];
|
||||
|
||||
if (pos < 0 || (uint64_t) pos >= cells.size()) {
|
||||
LLAMA_LOG_WARN("%s: MSA: position %d is outside the cache range [0, %u)\n",
|
||||
__func__, pos, cells.size());
|
||||
return { };
|
||||
}
|
||||
|
||||
const uint32_t idx = (uint32_t) pos;
|
||||
|
||||
if (!cells.is_empty(idx)) {
|
||||
LLAMA_LOG_WARN("%s: MSA: required slot %u is already occupied (stream %u)\n",
|
||||
__func__, idx, seq_to_stream[seq_id]);
|
||||
return { };
|
||||
}
|
||||
|
||||
// strictly increasing positions, rules out duplicates and, for contiguous requests, is tightened to exact adjacency
|
||||
if (!res.idxs[s].empty() && (cont ? idx != res.idxs[s].back() + 1
|
||||
: idx <= res.idxs[s].back())) {
|
||||
LLAMA_LOG_WARN("%s: MSA: token positions are not %s within the ubatch\n",
|
||||
__func__, cont ? "contiguous" : "strictly increasing");
|
||||
return { };
|
||||
}
|
||||
|
||||
res.idxs[s].push_back(idx);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t head_cur = v_heads[seq_to_stream[seq_id]];
|
||||
|
||||
// if we have enough unused cells before the current head ->
|
||||
@@ -1002,11 +1104,6 @@ llama_kv_cache::slot_info llama_kv_cache::find_slot(const llama_ubatch & ubatch,
|
||||
head_cur = 0;
|
||||
}
|
||||
|
||||
if (n_tokens > cells.size()) {
|
||||
LLAMA_LOG_ERROR("%s: n_tokens = %d > size = %u\n", __func__, n_tokens, cells.size());
|
||||
return { };
|
||||
}
|
||||
|
||||
uint32_t n_tested = 0;
|
||||
|
||||
// for continuous slots, we test that all tokens in the ubatch fit, starting from the current head
|
||||
@@ -1113,6 +1210,15 @@ void llama_kv_cache::apply_ubatch(const slot_info & sinfo, const llama_ubatch &
|
||||
|
||||
const auto idx = sinfo.idxs[s][ii];
|
||||
|
||||
if (msa_strict_slots && (llama_pos) idx != ubatch.pos[i]) {
|
||||
LLAMA_LOG_ERROR("%s: MSA slot/position invariant violated: "
|
||||
"writing pos %d into cell %u (stream %u). The indexer cache "
|
||||
"would desync and block selection would silently corrupt. "
|
||||
"This is a bug, please report it with reproduction steps.\n",
|
||||
__func__, ubatch.pos[i], idx, sinfo.strm[s]);
|
||||
GGML_ABORT("MSA: slot != pos");
|
||||
}
|
||||
|
||||
if (!cells.is_empty(idx)) {
|
||||
assert(cells.seq_count(idx) == 1);
|
||||
|
||||
@@ -1156,7 +1262,8 @@ void llama_kv_cache::apply_ubatch(const slot_info & sinfo, const llama_ubatch &
|
||||
LLAMA_LOG_DEBUG("%s: purging positions [%d, %d] of sequence %d from KV cache\n",
|
||||
__func__, cells.seq_pos_min(s), seq_pos_max_rm[s], s);
|
||||
|
||||
seq_rm(s, cells.seq_pos_min(s), seq_pos_max_rm[s] + 1);
|
||||
// under MSA strict slots this path should be unreachable, since strict MSA placement never selects occupied cells
|
||||
GGML_ASSERT(seq_rm(s, cells.seq_pos_min(s), seq_pos_max_rm[s] + 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1176,6 +1283,12 @@ bool llama_kv_cache::get_can_shift() const {
|
||||
if (hparams.n_pos_per_embd() > 1) {
|
||||
return false;
|
||||
}
|
||||
// shifting would leave k_idx stale
|
||||
for (const auto & layer : layers) {
|
||||
if (layer.k_idx) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1292,6 +1405,23 @@ ggml_tensor * llama_kv_cache::get_v(ggml_context * ctx, int32_t il, uint32_t n_k
|
||||
ggml_row_size(v->type, kv_size*n_embd_v_gqa)*sinfo.s0);
|
||||
}
|
||||
|
||||
ggml_tensor * llama_kv_cache::get_k_idx(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const {
|
||||
const int32_t ikv = map_layer_ids.at(il);
|
||||
auto * k_idx = layers[ikv].k_idx;
|
||||
GGML_ASSERT(k_idx);
|
||||
|
||||
const uint64_t kv_size = get_size();
|
||||
const int64_t n_idx = k_idx->ne[0]; // 128
|
||||
const uint32_t ns = sinfo.s1 - sinfo.s0 + 1;
|
||||
|
||||
return ggml_view_4d(ctx, k_idx,
|
||||
n_idx, 1, n_kv, ns,
|
||||
ggml_row_size(k_idx->type, n_idx), // nb1 (single head)
|
||||
ggml_row_size(k_idx->type, n_idx), // nb2 (per cell)
|
||||
ggml_row_size(k_idx->type, n_idx*kv_size), // nb3 (per stream)
|
||||
ggml_row_size(k_idx->type, n_idx*kv_size)*sinfo.s0);
|
||||
}
|
||||
|
||||
ggml_tensor * llama_kv_cache::cpy_k(ggml_context * ctx, ggml_tensor * k_cur, ggml_tensor * k_idxs, int32_t il, const slot_info & sinfo) const {
|
||||
GGML_UNUSED(sinfo);
|
||||
|
||||
@@ -1393,6 +1523,28 @@ ggml_tensor * llama_kv_cache::build_input_k_idxs(ggml_context * ctx, const llama
|
||||
return k_idxs;
|
||||
}
|
||||
|
||||
ggml_tensor * llama_kv_cache::cpy_k_idx(ggml_context * ctx, ggml_tensor * k_idx_cur, ggml_tensor * k_idxs, int32_t il, const slot_info & sinfo) const {
|
||||
GGML_UNUSED(sinfo);
|
||||
const int32_t ikv = map_layer_ids.at(il);
|
||||
ggml_tensor * k_idx = layers[ikv].k_idx;
|
||||
GGML_ASSERT(k_idx && "cpy_k_idx on a layer with no indexer cache");
|
||||
|
||||
const int64_t n_embd_head = k_idx_cur->ne[0]; // 128
|
||||
const int64_t n_head = k_idx_cur->ne[1]; // 1
|
||||
const int64_t n_tokens = k_idx_cur->ne[2];
|
||||
const int64_t n_embd_gqa = n_embd_head*n_head; // 128
|
||||
|
||||
GGML_ASSERT(ggml_row_size(k_idx_cur->type, n_embd_head) == k_idx_cur->nb[1]);
|
||||
k_idx_cur = ggml_view_2d(ctx, k_idx_cur, n_embd_gqa, n_tokens, k_idx_cur->nb[2], 0);
|
||||
|
||||
const int64_t n_stream = k_idx->ne[2];
|
||||
if (n_stream > 1) {
|
||||
const int64_t kv_size = get_size();
|
||||
k_idx = ggml_reshape_2d(ctx, k_idx, n_embd_gqa, kv_size*n_stream);
|
||||
}
|
||||
return ggml_set_rows(ctx, k_idx, k_idx_cur, k_idxs); // same k_idxs as the K store
|
||||
}
|
||||
|
||||
ggml_tensor * llama_kv_cache::build_input_v_idxs(ggml_context * ctx, const llama_ubatch & ubatch) const {
|
||||
const uint32_t n_tokens = ubatch.n_tokens;
|
||||
|
||||
@@ -1827,6 +1979,18 @@ size_t llama_kv_cache::size_v_bytes() const {
|
||||
return size_v_bytes;
|
||||
}
|
||||
|
||||
size_t llama_kv_cache::size_k_idx_bytes() const {
|
||||
size_t size_k_idx_bytes = 0;
|
||||
|
||||
for (const auto & layer : layers) {
|
||||
if (layer.k_idx) {
|
||||
size_k_idx_bytes += ggml_nbytes(layer.k_idx);
|
||||
}
|
||||
}
|
||||
|
||||
return size_k_idx_bytes;
|
||||
}
|
||||
|
||||
ggml_tensor * llama_kv_cache::build_rope_shift(
|
||||
const llama_cparams & cparams,
|
||||
ggml_context * ctx,
|
||||
@@ -2139,6 +2303,36 @@ void llama_kv_cache::state_write_data(llama_io_write_i & io, const cell_ranges_t
|
||||
}
|
||||
}
|
||||
|
||||
if (size_k_idx_bytes() > 0) {
|
||||
const uint32_t has_k_idx_u32 = 1;
|
||||
io.write(&has_k_idx_u32, sizeof(has_k_idx_u32));
|
||||
|
||||
for (const auto & layer : layers) {
|
||||
const uint32_t layer_has_k_idx = layer.k_idx ? 1 : 0;
|
||||
io.write(&layer_has_k_idx, sizeof(layer_has_k_idx));
|
||||
|
||||
if (!layer_has_k_idx) {
|
||||
continue;
|
||||
}
|
||||
|
||||
GGML_ASSERT(layer.k_idx_stream[cr.strm]);
|
||||
|
||||
const int32_t k_idx_type_i = (int32_t) layer.k_idx->type;
|
||||
io.write(&k_idx_type_i, sizeof(k_idx_type_i));
|
||||
|
||||
const uint64_t k_idx_size_row = ggml_row_size(layer.k_idx->type, layer.k_idx->ne[0]);
|
||||
io.write(&k_idx_size_row, sizeof(k_idx_size_row));
|
||||
|
||||
for (const auto & range : cr.data) {
|
||||
const size_t range_size = range.second - range.first;
|
||||
const size_t buf_size = range_size * k_idx_size_row;
|
||||
const size_t offset = range.first * k_idx_size_row;
|
||||
|
||||
io.write_tensor(layer.k_idx_stream[cr.strm], offset, buf_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!v_trans) {
|
||||
for (const auto & layer : layers) {
|
||||
const uint32_t il = layer.il;
|
||||
@@ -2387,6 +2581,68 @@ bool llama_kv_cache::state_read_data(llama_io_read_i & io, uint32_t strm, uint32
|
||||
}
|
||||
}
|
||||
|
||||
if (size_k_idx_bytes() > 0) {
|
||||
uint32_t has_k_idx_u32 = 0;
|
||||
io.read(&has_k_idx_u32, sizeof(has_k_idx_u32));
|
||||
|
||||
if (has_k_idx_u32 != 1) {
|
||||
LLAMA_LOG_ERROR("%s: missing k_idx data in KV cache state\n", __func__);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto & layer : layers) {
|
||||
uint32_t layer_has_k_idx = 0;
|
||||
io.read(&layer_has_k_idx, sizeof(layer_has_k_idx));
|
||||
|
||||
const uint32_t expected_layer_has_k_idx = layer.k_idx ? 1 : 0;
|
||||
|
||||
if (layer_has_k_idx != expected_layer_has_k_idx) {
|
||||
LLAMA_LOG_ERROR(
|
||||
"%s: mismatched k_idx state for layer: got %u, expected %u\n",
|
||||
__func__, layer_has_k_idx, expected_layer_has_k_idx);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!layer_has_k_idx) {
|
||||
continue;
|
||||
}
|
||||
|
||||
GGML_ASSERT(layer.k_idx_stream[strm]);
|
||||
|
||||
int32_t k_idx_type_i = -1;
|
||||
io.read(&k_idx_type_i, sizeof(k_idx_type_i));
|
||||
|
||||
if (k_idx_type_i != (int32_t) layer.k_idx->type) {
|
||||
LLAMA_LOG_ERROR(
|
||||
"%s: mismatched k_idx type: got %d, expected %d\n",
|
||||
__func__, k_idx_type_i, (int32_t) layer.k_idx->type);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t k_idx_size_row = 0;
|
||||
io.read(&k_idx_size_row, sizeof(k_idx_size_row));
|
||||
|
||||
const uint64_t expected_k_idx_size_row = ggml_row_size(layer.k_idx->type, layer.k_idx->ne[0]);
|
||||
|
||||
if (k_idx_size_row != expected_k_idx_size_row) {
|
||||
LLAMA_LOG_ERROR(
|
||||
"%s: mismatched k_idx row size: got %zu, expected %zu\n",
|
||||
__func__, (size_t) k_idx_size_row, (size_t) expected_k_idx_size_row);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cell_count) {
|
||||
if (sinfo.is_contiguous()) {
|
||||
io.read_tensor(layer.k_idx_stream[strm], sinfo.head() * k_idx_size_row, cell_count * k_idx_size_row);
|
||||
} else {
|
||||
for (uint32_t i = 0; i < cell_count; ++i) {
|
||||
io.read_tensor(layer.k_idx_stream[strm], sinfo.idxs[0][i] * k_idx_size_row, k_idx_size_row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->v_trans) {
|
||||
for (const auto & layer : layers) {
|
||||
const uint32_t il = layer.il;
|
||||
@@ -2588,6 +2844,10 @@ ggml_tensor * llama_kv_cache_context::get_v(ggml_context * ctx, int32_t il) cons
|
||||
return kv->get_v(ctx, il, n_kv, sinfos[i_cur]);
|
||||
}
|
||||
|
||||
ggml_tensor * llama_kv_cache_context::get_k_idx(ggml_context * ctx, int32_t il) const {
|
||||
return kv->get_k_idx(ctx, il, n_kv, sinfos[i_cur]);
|
||||
}
|
||||
|
||||
ggml_tensor * llama_kv_cache_context::cpy_k(ggml_context * ctx, ggml_tensor * k_cur, ggml_tensor * k_idxs, int32_t il) const {
|
||||
return kv->cpy_k(ctx, k_cur, k_idxs, il, sinfos[i_cur]);
|
||||
}
|
||||
@@ -2596,6 +2856,10 @@ ggml_tensor * llama_kv_cache_context::cpy_v(ggml_context * ctx, ggml_tensor * v_
|
||||
return kv->cpy_v(ctx, v_cur, v_idxs, il, sinfos[i_cur]);
|
||||
}
|
||||
|
||||
ggml_tensor * llama_kv_cache_context::cpy_k_idx(ggml_context * ctx, ggml_tensor * k_idx_cur, ggml_tensor * k_idxs, int32_t il) const {
|
||||
return kv->cpy_k_idx(ctx, k_idx_cur, k_idxs, il, sinfos[i_cur]);
|
||||
}
|
||||
|
||||
ggml_tensor * llama_kv_cache_context::build_input_k_idxs(ggml_context * ctx, const llama_ubatch & ubatch) const {
|
||||
return kv->build_input_k_idxs(ctx, ubatch);
|
||||
}
|
||||
|
||||
@@ -173,10 +173,12 @@ public:
|
||||
// get views of the current state of the cache
|
||||
ggml_tensor * get_k(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const;
|
||||
ggml_tensor * get_v(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const;
|
||||
ggml_tensor * get_k_idx(ggml_context * ctx, int32_t il, uint32_t n_kv, const slot_info & sinfo) const;
|
||||
|
||||
// store k_cur and v_cur in the cache based on the provided head location
|
||||
ggml_tensor * cpy_k(ggml_context * ctx, ggml_tensor * k_cur, ggml_tensor * k_idxs, int32_t il, const slot_info & sinfo) const;
|
||||
ggml_tensor * cpy_v(ggml_context * ctx, ggml_tensor * v_cur, ggml_tensor * v_idxs, int32_t il, const slot_info & sinfo) const;
|
||||
ggml_tensor * cpy_k_idx(ggml_context * ctx, ggml_tensor * k_idx_cur, ggml_tensor * k_idxs, int32_t il, const slot_info & sinfo) const;
|
||||
|
||||
//
|
||||
// preparation API
|
||||
@@ -228,9 +230,11 @@ private:
|
||||
|
||||
ggml_tensor * k;
|
||||
ggml_tensor * v;
|
||||
ggml_tensor * k_idx; // MSA single-head indexer keys, F32
|
||||
|
||||
std::vector<ggml_tensor *> k_stream;
|
||||
std::vector<ggml_tensor *> v_stream;
|
||||
std::vector<ggml_tensor *> k_idx_stream;
|
||||
};
|
||||
|
||||
bool v_trans = true; // the value tensor is transposed
|
||||
@@ -259,6 +263,9 @@ private:
|
||||
// env: LLAMA_KV_CACHE_DEBUG
|
||||
int debug = 0;
|
||||
|
||||
// set when a k_idx (indexer) cache exists and the stream layout supports MSA (single seq, or one stream per seq)
|
||||
bool msa_strict_slots = false;
|
||||
|
||||
// this is the SWA type of the cache - not to be confused with the model SWA type
|
||||
const llama_swa_type swa_type = LLAMA_SWA_TYPE_NONE;
|
||||
|
||||
@@ -291,6 +298,7 @@ private:
|
||||
|
||||
size_t size_k_bytes() const;
|
||||
size_t size_v_bytes() const;
|
||||
size_t size_k_idx_bytes() const;
|
||||
|
||||
ggml_tensor * build_rope_shift(
|
||||
const llama_cparams & cparams,
|
||||
@@ -370,6 +378,7 @@ public:
|
||||
// get views of the current state of the cache
|
||||
ggml_tensor * get_k(ggml_context * ctx, int32_t il) const;
|
||||
ggml_tensor * get_v(ggml_context * ctx, int32_t il) const;
|
||||
ggml_tensor * get_k_idx(ggml_context * ctx, int32_t il) const;
|
||||
|
||||
// store k_cur and v_cur in the cache based on the provided head location
|
||||
// note: the heads in k_cur and v_cur should be laid out contiguously in memory
|
||||
@@ -379,6 +388,7 @@ public:
|
||||
// - v_idxs [n_tokens] or [n_tokens*n_embd_v_gqa] depending if V cache is transposed
|
||||
ggml_tensor * cpy_k(ggml_context * ctx, ggml_tensor * k_cur, ggml_tensor * k_idxs, int32_t il) const;
|
||||
ggml_tensor * cpy_v(ggml_context * ctx, ggml_tensor * v_cur, ggml_tensor * v_idxs, int32_t il) const;
|
||||
ggml_tensor * cpy_k_idx(ggml_context * ctx, ggml_tensor * k_idx_cur, ggml_tensor * k_idxs, int32_t il) const;
|
||||
|
||||
// create destination indices for each head of the current batch for where it would be written in the KV cache
|
||||
// the indices address the global KV cache (not per stream) - this is not relevant for the user of this API, but
|
||||
|
||||
@@ -542,7 +542,7 @@ llama_model_loader::llama_model_loader(
|
||||
|
||||
tensor_buft_overrides = param_tensor_buft_overrides_p;
|
||||
|
||||
this->use_mmap = load_mode == LLAMA_LOAD_MODE_MMAP || load_mode == LLAMA_LOAD_MODE_MLOCK;
|
||||
this->use_mmap = load_mode == LLAMA_LOAD_MODE_MMAP || load_mode == LLAMA_LOAD_MODE_MMAP_MLOCK;
|
||||
this->use_direct_io = load_mode == LLAMA_LOAD_MODE_DIRECT_IO;
|
||||
|
||||
if (!fname.empty()) {
|
||||
|
||||
@@ -281,6 +281,8 @@ void llama_model_saver::add_kv_from_model() {
|
||||
add_kv(LLM_KV_ATTENTION_INDEXER_HEAD_COUNT, hparams.indexer_n_head);
|
||||
add_kv(LLM_KV_ATTENTION_INDEXER_KEY_LENGTH, hparams.indexer_head_size);
|
||||
add_kv(LLM_KV_ATTENTION_INDEXER_TOP_K, hparams.indexer_top_k);
|
||||
add_kv(LLM_KV_ATTENTION_INDEXER_BLOCK_SIZE, hparams.indexer_block_size);
|
||||
add_kv(LLM_KV_ATTENTION_INDEXER_LOCAL_BLOCKS, hparams.indexer_local_blocks);
|
||||
add_kv(LLM_KV_ATTENTION_INDEXER_TYPES, hparams.is_indexer_full_impl, true);
|
||||
add_kv(LLM_KV_ATTENTION_RECURRENT_LAYERS, hparams.is_recr_impl, true);
|
||||
|
||||
|
||||
+8
-1
@@ -85,6 +85,8 @@ static llama_model * llama_model_mapping(llm_arch arch, const llama_model_params
|
||||
return new llama_model_stablelm(params);
|
||||
case LLM_ARCH_MELLUM:
|
||||
return new llama_model_mellum(params);
|
||||
case LLM_ARCH_NANBEIGE:
|
||||
return new llama_model_nanbeige(params);
|
||||
case LLM_ARCH_QWEN:
|
||||
return new llama_model_qwen(params);
|
||||
case LLM_ARCH_QWEN2:
|
||||
@@ -285,6 +287,8 @@ static llama_model * llama_model_mapping(llm_arch arch, const llama_model_params
|
||||
return new llama_model_apertus(params);
|
||||
case LLM_ARCH_MINIMAX_M2:
|
||||
return new llama_model_minimax_m2(params);
|
||||
case LLM_ARCH_MINIMAX_M3:
|
||||
return new llama_model_minimax_m3(params);
|
||||
case LLM_ARCH_COGVLM:
|
||||
return new llama_model_cogvlm(params);
|
||||
case LLM_ARCH_PANGU_EMBED:
|
||||
@@ -818,6 +822,7 @@ const char * llm_type_name(llm_type type) {
|
||||
case LLM_TYPE_122B_A10B: return "122B.A10B";
|
||||
case LLM_TYPE_196B_A11B: return "196B.A11B";
|
||||
case LLM_TYPE_230B_A10B: return "230B.A10B";
|
||||
case LLM_TYPE_428B_A23B: return "428B.A23B";
|
||||
case LLM_TYPE_235B_A22B: return "235B.A22B";
|
||||
case LLM_TYPE_300B_A47B: return "300B.A47B";
|
||||
case LLM_TYPE_310B_A15B: return "310B.A15B";
|
||||
@@ -1246,7 +1251,7 @@ void llama_model_base::load_vocab(llama_model_loader & ml) {
|
||||
|
||||
bool llama_model_base::load_tensors(llama_model_loader & ml) {
|
||||
const auto & split_mode = params.split_mode;
|
||||
const bool use_mlock = params.load_mode == LLAMA_LOAD_MODE_MLOCK;
|
||||
const bool use_mlock = params.load_mode == LLAMA_LOAD_MODE_MLOCK || params.load_mode == LLAMA_LOAD_MODE_MMAP_MLOCK;
|
||||
const auto & tensor_split = params.tensor_split;
|
||||
|
||||
const int n_layer_all = hparams.n_layer_all;
|
||||
@@ -2488,6 +2493,7 @@ llama_rope_type llama_model_rope_type(const llama_model * model) {
|
||||
case LLM_ARCH_LLAMA_EMBED:
|
||||
case LLM_ARCH_MAINCODER:
|
||||
case LLM_ARCH_GLM_DSA:
|
||||
case LLM_ARCH_NANBEIGE:
|
||||
return LLAMA_ROPE_TYPE_NORM;
|
||||
|
||||
// the pairs of head values are offset by n_rot/2
|
||||
@@ -2550,6 +2556,7 @@ llama_rope_type llama_model_rope_type(const llama_model * model) {
|
||||
case LLM_ARCH_GROVEMOE:
|
||||
case LLM_ARCH_APERTUS:
|
||||
case LLM_ARCH_MINIMAX_M2:
|
||||
case LLM_ARCH_MINIMAX_M3:
|
||||
case LLM_ARCH_COGVLM:
|
||||
case LLM_ARCH_PANGU_EMBED:
|
||||
case LLM_ARCH_AFMOE:
|
||||
|
||||
@@ -134,6 +134,7 @@ enum llm_type {
|
||||
LLM_TYPE_122B_A10B, // Qwen3.5
|
||||
LLM_TYPE_196B_A11B, // Step3.5-Flash
|
||||
LLM_TYPE_230B_A10B, // Minimax M2
|
||||
LLM_TYPE_428B_A23B, // Minimax M3
|
||||
LLM_TYPE_235B_A22B,
|
||||
LLM_TYPE_300B_A47B, // Ernie MoE big
|
||||
LLM_TYPE_310B_A15B, // /MiMo-V2-Flash
|
||||
@@ -515,6 +516,12 @@ struct llama_layer {
|
||||
struct ggml_tensor * indexer_attn_k = nullptr;
|
||||
struct ggml_tensor * indexer_attn_q_b = nullptr; // note: for lora a/b, not bias
|
||||
|
||||
// MSA
|
||||
struct ggml_tensor * index_q_proj = nullptr;
|
||||
struct ggml_tensor * index_k_proj = nullptr;
|
||||
struct ggml_tensor * index_q_norm = nullptr;
|
||||
struct ggml_tensor * index_k_norm = nullptr;
|
||||
|
||||
// gemma4 layer output scale, reused for talkie embedding skip scale
|
||||
struct ggml_tensor * out_scale = nullptr;
|
||||
|
||||
|
||||
@@ -326,6 +326,10 @@ static bool tensor_allows_quantization(const llama_model_quantize_params * param
|
||||
quantize &= name.find("ssm_conv1d") == std::string::npos;
|
||||
quantize &= name.find("shortconv.conv.weight") == std::string::npos;
|
||||
|
||||
// do not quantize MiniMax's indexer projection weights, they are tiny
|
||||
quantize &= name.find("indexer.k_proj.weight") == std::string::npos;
|
||||
quantize &= name.find("indexer.q_proj.weight") == std::string::npos;
|
||||
|
||||
// do not quantize RWKV's small yet 2D weights
|
||||
quantize &= name.find("time_mix_first.weight") == std::string::npos;
|
||||
quantize &= name.find("time_mix_w0.weight") == std::string::npos;
|
||||
@@ -355,6 +359,10 @@ static bool tensor_allows_quantization(const llama_model_quantize_params * param
|
||||
quantize &= name.find(".patch_embd") == std::string::npos;
|
||||
quantize &= name.find(".patch_merger") == std::string::npos;
|
||||
|
||||
// audio codebook
|
||||
quantize &= name.find("a.rvq.codebook") == std::string::npos;
|
||||
quantize &= name.find("mm.a.code_embd") == std::string::npos;
|
||||
|
||||
return quantize;
|
||||
}
|
||||
|
||||
|
||||
@@ -2809,6 +2809,7 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
|
||||
|| t.first == "<turn|>" // gemma4
|
||||
|| t.first == "<|tool_response>" // gemma4
|
||||
|| t.first == "<|end▁of▁sentence|>" // deepseek-ocr
|
||||
|| t.first == "[e~[" // minimax-m2/m3
|
||||
) {
|
||||
special_eog_ids.insert(t.second);
|
||||
if ((attr & LLAMA_TOKEN_ATTR_CONTROL) == 0) {
|
||||
|
||||
+7
-4
@@ -54,6 +54,8 @@ const char * llama_load_mode_name(enum llama_load_mode load_mode) {
|
||||
return "mmap";
|
||||
case LLAMA_LOAD_MODE_MLOCK:
|
||||
return "mlock";
|
||||
case LLAMA_LOAD_MODE_MMAP_MLOCK:
|
||||
return "mmap+mlock";
|
||||
case LLAMA_LOAD_MODE_DIRECT_IO:
|
||||
return "dio";
|
||||
}
|
||||
@@ -61,10 +63,11 @@ const char * llama_load_mode_name(enum llama_load_mode load_mode) {
|
||||
}
|
||||
|
||||
enum llama_load_mode llama_load_mode_from_str(const char * str) {
|
||||
if (std::strcmp(str, "none") == 0) { return LLAMA_LOAD_MODE_NONE; }
|
||||
if (std::strcmp(str, "mmap") == 0) { return LLAMA_LOAD_MODE_MMAP; }
|
||||
if (std::strcmp(str, "mlock") == 0) { return LLAMA_LOAD_MODE_MLOCK; }
|
||||
if (std::strcmp(str, "dio") == 0) { return LLAMA_LOAD_MODE_DIRECT_IO; }
|
||||
if (std::strcmp(str, "none") == 0) { return LLAMA_LOAD_MODE_NONE; }
|
||||
if (std::strcmp(str, "mmap") == 0) { return LLAMA_LOAD_MODE_MMAP; }
|
||||
if (std::strcmp(str, "mlock") == 0) { return LLAMA_LOAD_MODE_MLOCK; }
|
||||
if (std::strcmp(str, "mmap+mlock") == 0) { return LLAMA_LOAD_MODE_MMAP_MLOCK; }
|
||||
if (std::strcmp(str, "dio") == 0) { return LLAMA_LOAD_MODE_DIRECT_IO; }
|
||||
throw std::invalid_argument(std::string("unknown load mode: ") + str);
|
||||
}
|
||||
|
||||
|
||||
@@ -1133,6 +1133,10 @@ llama_model_deepseek4::graph::graph(const llama_model & model, const llm_graph_p
|
||||
&post, &comb, il);
|
||||
cb(cur, "hc_ffn_pre", il);
|
||||
|
||||
ggml_build_forward_expand(gf, residual);
|
||||
ggml_build_forward_expand(gf, post);
|
||||
ggml_build_forward_expand(gf, comb);
|
||||
|
||||
cur = build_norm(cur, model.layers[il].ffn_norm, nullptr, LLM_NORM_RMS, il);
|
||||
cb(cur, "ffn_norm", il);
|
||||
|
||||
@@ -1175,7 +1179,7 @@ llama_model_deepseek4::graph::graph(const llama_model & model, const llm_graph_p
|
||||
|
||||
inpL = build_hc_post(cur, residual, post, comb, il);
|
||||
inpL = build_cvec(inpL, il);
|
||||
cb(inpL, "l_out", il);
|
||||
cb(inpL, "l_last", il);
|
||||
}
|
||||
|
||||
if (inp_out_ids) {
|
||||
|
||||
@@ -28,6 +28,10 @@ void llama_model_eagle3::load_arch_hparams(llama_model_loader & ml) {
|
||||
LLAMA_LOG_INFO("%s: EAGLE3gnorm_before_residual = true\n", __func__);
|
||||
}
|
||||
|
||||
// eagle3 norm_before_fc (optional, default false)
|
||||
// compatible with eagle3.1 (e.g. nvidia/gpt-oss-120b-Eagle3-v3)
|
||||
ml.get_key(LLM_KV_NORM_BEFORE_FC, hparams.norm_before_fc, false);
|
||||
|
||||
type = LLM_TYPE_UNKNOWN;
|
||||
}
|
||||
|
||||
@@ -53,6 +57,11 @@ void llama_model_eagle3::load_arch_tensors(llama_model_loader &) {
|
||||
// Feature fusion layer: projects 3 target layers to draft hidden size
|
||||
fc = create_tensor(tn(LLM_TENSOR_FC, "weight"), {n_embd_inp, n_embd}, 0);
|
||||
|
||||
// RMSNorm on the fused target features (input to fc), only when norm_before_fc is set.
|
||||
if (hparams.norm_before_fc) {
|
||||
output_norm_enc = create_tensor(tn(LLM_TENSOR_ENC_OUTPUT_NORM, "weight"), {n_embd_inp}, 0);
|
||||
}
|
||||
|
||||
// Output layer (uses draft vocab size)
|
||||
output_norm = create_tensor(tn(LLM_TENSOR_OUTPUT_NORM, "weight"), {n_embd}, 0);
|
||||
output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), {n_embd, n_draft_vocab}, TENSOR_NOT_REQUIRED);
|
||||
@@ -130,6 +139,12 @@ llama_model_eagle3::graph<true>::graph(const llama_model & model, const llm_grap
|
||||
|
||||
cur = build_inp_embd_enc();
|
||||
|
||||
// RMSNorm on the fused target features before fc
|
||||
if (hparams.norm_before_fc) {
|
||||
cur = build_norm(cur, model.output_norm_enc, NULL, LLM_NORM_RMS, -1);
|
||||
cb(cur, "enc_input_norm", -1);
|
||||
}
|
||||
|
||||
// Feature fusion layer
|
||||
cur = build_lora_mm(model.fc, cur);
|
||||
cb(cur, "fc_out", -1);
|
||||
|
||||
@@ -0,0 +1,562 @@
|
||||
#include "models.h"
|
||||
#include "llama-kv-cache.h"
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
// MiniMax-M3: MiniMax-M2 style GQA (per-head QK-norm, partial rotary) with
|
||||
// DeepSeek-V3 leading-dense + routed/shared experts (sigmoid gating, routed scaling),
|
||||
// swigluoai activation, and MiniMax Sparse Attention (MSA). MTP is not in released model weights.
|
||||
// Notes: Blocks are anchored to absolute KV cache slots.
|
||||
|
||||
void llama_model_minimax_m3::load_arch_hparams(llama_model_loader & ml) {
|
||||
ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps);
|
||||
ml.get_key(LLM_KV_LEADING_DENSE_BLOCK_COUNT, hparams.n_layer_dense_lead, false);
|
||||
ml.get_key(LLM_KV_EXPERT_FEED_FORWARD_LENGTH, hparams.n_ff_exp);
|
||||
ml.get_key(LLM_KV_EXPERT_SHARED_COUNT, hparams.n_expert_shared);
|
||||
ml.get_key(LLM_KV_EXPERT_WEIGHTS_SCALE, hparams.expert_weights_scale, false);
|
||||
ml.get_key(LLM_KV_EXPERT_WEIGHTS_NORM, hparams.expert_weights_norm, false);
|
||||
ml.get_key(LLM_KV_EXPERT_GATING_FUNC, hparams.expert_gating_func);
|
||||
ml.get_key(LLM_KV_ATTENTION_INDEXER_HEAD_COUNT, hparams.indexer_n_head);
|
||||
ml.get_key(LLM_KV_ATTENTION_INDEXER_KEY_LENGTH, hparams.indexer_head_size);
|
||||
ml.get_key(LLM_KV_ATTENTION_INDEXER_TOP_K, hparams.indexer_top_k);
|
||||
ml.get_key(LLM_KV_ATTENTION_INDEXER_BLOCK_SIZE, hparams.indexer_block_size);
|
||||
ml.get_key(LLM_KV_ATTENTION_INDEXER_LOCAL_BLOCKS, hparams.indexer_local_blocks);
|
||||
msa_p = { (int) hparams.indexer_block_size, (int) hparams.indexer_top_k, (int) hparams.indexer_local_blocks };
|
||||
hparams.indexer_kv = true;
|
||||
|
||||
switch (hparams.n_layer()) {
|
||||
case 60: type = LLM_TYPE_428B_A23B; break;
|
||||
default: type = LLM_TYPE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
void llama_model_minimax_m3::load_arch_tensors(llama_model_loader &) {
|
||||
LLAMA_LOAD_LOCALS;
|
||||
const int64_t n_expert_shared = hparams.n_expert_shared;
|
||||
const int64_t n_ff_exp = hparams.n_ff_exp;
|
||||
|
||||
tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0);
|
||||
|
||||
// output
|
||||
output_norm = create_tensor(tn(LLM_TENSOR_OUTPUT_NORM, "weight"), {n_embd}, 0);
|
||||
output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), {n_embd, n_vocab}, 0);
|
||||
|
||||
for (int i = 0; i < n_layer; ++i) {
|
||||
auto & layer = layers[i];
|
||||
|
||||
create_tensor_qkv(layer, i, n_embd, n_embd_head_k * n_head, n_embd_gqa, n_embd_gqa, 0);
|
||||
layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), { n_embd_head_k * n_head, n_embd }, 0);
|
||||
|
||||
layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0);
|
||||
// per-head QK-norm: a single head_dim vector applied to every head
|
||||
layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", i), {n_embd_head_k}, 0);
|
||||
layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", i), {n_embd_head_k}, 0);
|
||||
|
||||
layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, 0);
|
||||
|
||||
if (i < (int) hparams.n_layer_dense_lead) {
|
||||
// leading dense layers
|
||||
layer.ffn_gate = create_tensor(tn(LLM_TENSOR_FFN_GATE, "weight", i), {n_embd, n_ff}, 0);
|
||||
layer.ffn_down = create_tensor(tn(LLM_TENSOR_FFN_DOWN, "weight", i), { n_ff, n_embd}, 0);
|
||||
layer.ffn_up = create_tensor(tn(LLM_TENSOR_FFN_UP, "weight", i), {n_embd, n_ff}, 0);
|
||||
} else {
|
||||
// routed experts
|
||||
layer.ffn_gate_inp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP, "weight", i), {n_embd, n_expert}, 0);
|
||||
layer.ffn_exp_probs_b = create_tensor(tn(LLM_TENSOR_FFN_EXP_PROBS_B, "bias", i), {n_expert}, 0);
|
||||
layer.ffn_gate_exps = create_tensor(tn(LLM_TENSOR_FFN_GATE_EXPS, "weight", i), {n_embd, n_ff_exp, n_expert}, 0);
|
||||
layer.ffn_down_exps = create_tensor(tn(LLM_TENSOR_FFN_DOWN_EXPS, "weight", i), {n_ff_exp, n_embd, n_expert}, 0);
|
||||
layer.ffn_up_exps = create_tensor(tn(LLM_TENSOR_FFN_UP_EXPS, "weight", i), {n_embd, n_ff_exp, n_expert}, 0);
|
||||
|
||||
// shared expert
|
||||
layer.ffn_gate_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_SHEXP, "weight", i), {n_embd, n_ff_exp * n_expert_shared}, 0);
|
||||
layer.ffn_down_shexp = create_tensor(tn(LLM_TENSOR_FFN_DOWN_SHEXP, "weight", i), { n_ff_exp * n_expert_shared, n_embd}, 0);
|
||||
layer.ffn_up_shexp = create_tensor(tn(LLM_TENSOR_FFN_UP_SHEXP, "weight", i), {n_embd, n_ff_exp * n_expert_shared}, 0);
|
||||
|
||||
// indexer
|
||||
layer.index_q_proj = create_tensor(tn(LLM_TENSOR_INDEXER_Q_PROJ, "weight", i), {n_embd, hparams.indexer_n_head * hparams.indexer_head_size}, 0);
|
||||
layer.index_k_proj = create_tensor(tn(LLM_TENSOR_INDEXER_K_PROJ, "weight", i), {n_embd, hparams.indexer_head_size}, 0);
|
||||
layer.index_q_norm = create_tensor(tn(LLM_TENSOR_INDEXER_Q_NORM, "weight", i), {hparams.indexer_head_size}, 0);
|
||||
layer.index_k_norm = create_tensor(tn(LLM_TENSOR_INDEXER_K_NORM, "weight", i), {hparams.indexer_head_size}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<llm_graph_context> llama_model_minimax_m3::build_arch_graph(const llm_graph_params & params) const {
|
||||
return std::make_unique<graph>(*this, params);
|
||||
}
|
||||
|
||||
// per-query local-force bias for MSA selection
|
||||
// local window always wins a slot
|
||||
class llm_graph_input_msa_local : public llm_graph_input_i {
|
||||
public:
|
||||
llm_graph_input_msa_local(int blk, int local, int64_t nblk) : blk(blk), local(local), nblk(nblk) {}
|
||||
|
||||
void set_input(const llama_ubatch * ubatch) override {
|
||||
if (!bias || !ubatch->pos) {
|
||||
return;
|
||||
}
|
||||
const int64_t n_tokens = ubatch->n_tokens;
|
||||
std::vector<float> data((size_t) nblk * n_tokens, 0.0f);
|
||||
for (int64_t i = 0; i < n_tokens; ++i) {
|
||||
const int64_t L = ubatch->pos[i] / blk;
|
||||
for (int l = 0; l < local && L - l >= 0; ++l) {
|
||||
if (L - l < nblk) {
|
||||
data[(size_t) i * nblk + (L - l)] = 1e30f;
|
||||
}
|
||||
}
|
||||
}
|
||||
ggml_backend_tensor_set(bias, data.data(), 0, data.size() * sizeof(float));
|
||||
}
|
||||
|
||||
// valid as long as the bias tensor dims still match the new ubatch/cache window
|
||||
bool can_reuse(const llm_graph_params & params) override {
|
||||
const auto * mctx = static_cast<const llama_kv_cache_context *>(params.mctx);
|
||||
|
||||
bool res = true;
|
||||
res &= bias->ne[1] == params.ubatch.n_tokens;
|
||||
res &= bias->ne[0] * blk == (int64_t) mctx->get_n_kv();
|
||||
return res;
|
||||
}
|
||||
|
||||
ggml_tensor * bias = nullptr;
|
||||
int blk;
|
||||
int local;
|
||||
int64_t nblk;
|
||||
};
|
||||
|
||||
// pooled score of a block with no visible token: -inf from the mask, or -FLT_MAX from the
|
||||
// max-pool identity when every element of the block is -inf
|
||||
static inline bool msa_score_masked(float x) { return x <= -1e30f; }
|
||||
|
||||
// MSA block selection (batch regime)
|
||||
// CPU custom op, the token-level expansion and the combination with the causal mask happen on the GPU.
|
||||
static void msa_block_mask_op(struct ggml_tensor * dst, int ith, int nth, void * userdata) {
|
||||
const struct ggml_tensor * bs = dst->src[0];
|
||||
const struct ggml_tensor * bias = dst->src[1];
|
||||
const msa_params * p = (const msa_params *) userdata;
|
||||
|
||||
const int nblk = (int) bs->ne[0];
|
||||
const int Hd = (int) bs->ne[1];
|
||||
const int S = (int) bs->ne[2];
|
||||
|
||||
GGML_ASSERT(bs->type == GGML_TYPE_F32 && ggml_is_contiguous(bs));
|
||||
GGML_ASSERT(bias->type == GGML_TYPE_F32 && ggml_is_contiguous(bias));
|
||||
GGML_ASSERT(dst->type == GGML_TYPE_F16 && ggml_is_contiguous(dst));
|
||||
GGML_ASSERT(dst->ne[0] == nblk && dst->ne[1] == S && dst->ne[2] == Hd);
|
||||
GGML_ASSERT(bias->ne[0] == nblk && bias->ne[1] == S);
|
||||
|
||||
const int topk = p->topk_blocks < nblk ? p->topk_blocks : nblk;
|
||||
|
||||
const ggml_fp16_t f16_zero = ggml_fp32_to_fp16(0.0f);
|
||||
const ggml_fp16_t f16_ninf = ggml_fp32_to_fp16(-INFINITY);
|
||||
|
||||
std::vector<float> rank(nblk);
|
||||
std::vector<char> valid(nblk);
|
||||
std::vector<int> ord(nblk);
|
||||
|
||||
ggml_fp16_t * out = (ggml_fp16_t *) dst->data;
|
||||
|
||||
for (int i = ith; i < S; i += nth) {
|
||||
const float * bias_col = (const float *) bias->data + (size_t) i * nblk;
|
||||
for (int h = 0; h < Hd; ++h) {
|
||||
const float * bs_col = (const float *) bs->data + ((size_t) i * Hd + h) * nblk;
|
||||
|
||||
for (int bk = 0; bk < nblk; ++bk) {
|
||||
// a block is selectable if it has a visible token or is locally forced
|
||||
valid[bk] = !msa_score_masked(bs_col[bk]) || bias_col[bk] > 0.0f;
|
||||
rank [bk] = bias_col[bk] > 0.0f ? bias_col[bk] : bs_col[bk];
|
||||
ord [bk] = bk;
|
||||
}
|
||||
|
||||
std::partial_sort(ord.begin(), ord.begin() + topk, ord.end(),
|
||||
[&](int a, int b) { return rank[a] > rank[b]; });
|
||||
|
||||
ggml_fp16_t * dst_col = out + ((size_t) h * S + i) * nblk;
|
||||
for (int bk = 0; bk < nblk; ++bk) {
|
||||
dst_col[bk] = f16_ninf;
|
||||
}
|
||||
for (int t = 0; t < topk; ++t) {
|
||||
const int bk = ord[t];
|
||||
if (!valid[bk]) {
|
||||
break; // sorted desc: first invalid -> fewer than topk selectable blocks
|
||||
}
|
||||
dst_col[bk] = f16_zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One FA call for all GQA groups (and at multi-stream decode, all streams) by mapping them onto the FA sequence dim (ne[3])
|
||||
ggml_tensor * llama_model_minimax_m3::graph::build_attn_msa_fa(
|
||||
ggml_tensor * q_cur, // [D, HQ, T]
|
||||
ggml_tensor * k, // [D, n_keys, 1, C]
|
||||
ggml_tensor * v, // [D, n_keys, 1, C]
|
||||
ggml_tensor * mask, // [n_keys, R, 1, C] f16, contiguous
|
||||
int64_t Gp, float kq_scale, int il) const {
|
||||
|
||||
const int64_t D = q_cur->ne[0];
|
||||
const int64_t HQ = q_cur->ne[1];
|
||||
const int64_t T = q_cur->ne[2];
|
||||
const int64_t C = k->ne[3];
|
||||
const int64_t R = HQ*T/(Gp*C);
|
||||
GGML_ASSERT(Gp*C*R == HQ*T);
|
||||
GGML_ASSERT(mask->type == GGML_TYPE_F16);
|
||||
|
||||
// [D, HQ, T] -> [D, Gp, C, R] -> [D, R, Gp, C]
|
||||
// batch (C=HKV, R=T): channel = group
|
||||
// decode (C=HKV*ns, R=1): channel = (group, stream), group innermost
|
||||
ggml_tensor * q = ggml_reshape_4d(ctx0, q_cur, D, Gp, C, R);
|
||||
q = ggml_permute(ctx0, q, 0, 2, 3, 1);
|
||||
|
||||
ggml_tensor * o = ggml_flash_attn_ext(ctx0, q, k, v, mask, kq_scale,
|
||||
hparams.f_max_alibi_bias, 0.0f);
|
||||
ggml_flash_attn_ext_set_prec(o, GGML_PREC_F32);
|
||||
cb(o, "msa_fattn", il);
|
||||
|
||||
// [D, Gp, R, C] -> [D, Gp, C, R] -> [n_embd, T]
|
||||
o = ggml_permute(ctx0, o, 0, 1, 3, 2);
|
||||
if (!ggml_is_contiguous(o)) {
|
||||
o = ggml_cont(ctx0, o); // no-op layout at decode (R == 1), copy at batch
|
||||
}
|
||||
return ggml_reshape_2d(ctx0, o, D*HQ, T);
|
||||
}
|
||||
|
||||
llama_model_minimax_m3::graph::graph(const llama_model & model, const llm_graph_params & params) : llm_graph_context(params) {
|
||||
const int64_t n_embd_head = hparams.n_embd_head_v();
|
||||
const auto & mm = static_cast<const llama_model_minimax_m3 &>(model);
|
||||
|
||||
GGML_ASSERT(n_embd_head == hparams.n_embd_head_k());
|
||||
// partial rotary: head_dim != n_rot, so don't assert n_embd_head == n_rot
|
||||
|
||||
ggml_tensor * cur;
|
||||
ggml_tensor * inpL;
|
||||
|
||||
inpL = build_inp_embd(model.tok_embd);
|
||||
|
||||
ggml_tensor * inp_pos = build_inp_pos();
|
||||
auto inp_attn = build_attn_inp_kv();
|
||||
|
||||
// MSA calls ggml_flash_attn_ext directly and assumes the non-transposed V layout that
|
||||
// llama.cpp only provides when flash attention is enabled. Block selection is anchored
|
||||
// to absolute KV cache slots, which equal positions only for append-only per-stream
|
||||
// caches either a single sequence, or multiple sequences with kv_unified == false (each
|
||||
// stream then has its own slot space). A unified cache with multiple sequences
|
||||
// interleaves slots and would silently break block anchoring so it falls back to dense.
|
||||
const bool fa_on = cparams.flash_attn;
|
||||
const bool streams_ok = cparams.n_seq_max == 1 || !cparams.kv_unified;
|
||||
const bool msa_enabled = fa_on && streams_ok;
|
||||
|
||||
static bool warned_no_fa = false;
|
||||
if (!fa_on && !warned_no_fa) {
|
||||
LLAMA_LOG_WARN("%s: flash attention disabled; MSA requires it -> running DENSE attention "
|
||||
"(output may be degraded). Enable flash attention for MSA.\n", __func__);
|
||||
warned_no_fa = true;
|
||||
}
|
||||
static bool warned_unified = false;
|
||||
if (fa_on && !streams_ok && !warned_unified) {
|
||||
LLAMA_LOG_WARN("%s: unified KV cache with n_seq_max > 1; MSA needs per-sequence streams "
|
||||
"-> running DENSE attention. Output may be degraded. Drop --kv-unified to enable MSA.\n", __func__);
|
||||
warned_unified = true;
|
||||
}
|
||||
|
||||
// hoisted per-graph MSA state (shared by every sparse layer)
|
||||
llm_graph_input_msa_local * msa_loc = nullptr;
|
||||
ggml_tensor * msa_kqm = nullptr;
|
||||
ggml_tensor * msa_mf = nullptr;
|
||||
int64_t n_kv = 0, nblk = 0, ns = 1, n_tps = 0;
|
||||
bool msa_decode = false; // gather (1 token per stream) vs mask
|
||||
const int blk = mm.msa_p.blk;
|
||||
const int64_t Hd = hparams.indexer_n_head; // one indexer head per GQA group
|
||||
|
||||
if (msa_enabled) {
|
||||
msa_kqm = inp_attn->get_kq_mask();
|
||||
n_kv = msa_kqm->ne[0];
|
||||
n_tps = msa_kqm->ne[1]; // tokens per stream
|
||||
ns = msa_kqm->ne[3]; // streams in this ubatch
|
||||
GGML_ASSERT(msa_kqm->type == GGML_TYPE_F16 && "MSA requires the FA (f16) mask");
|
||||
GGML_ASSERT(n_tps*ns == n_tokens);
|
||||
GGML_ASSERT(n_kv % blk == 0 &&
|
||||
"MSA: KV/mask n_kv must be a multiple of indexer.block_size (128); "
|
||||
"the flash-attention KV padding must be a multiple of the block size. "
|
||||
"A non-multiple would silently drop the partial tail block.");
|
||||
nblk = n_kv / blk;
|
||||
msa_decode = n_tps == 1;
|
||||
|
||||
msa_mf = ggml_cast(ctx0, msa_kqm, GGML_TYPE_F32);
|
||||
|
||||
auto loc = std::make_unique<llm_graph_input_msa_local>(blk, mm.msa_p.local, nblk);
|
||||
loc->bias = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, nblk, n_tokens); // stream-grouped tokens
|
||||
ggml_set_input(loc->bias);
|
||||
msa_loc = (llm_graph_input_msa_local *) res->add_input(std::move(loc));
|
||||
}
|
||||
|
||||
ggml_tensor * inp_out_ids = build_inp_out_ids();
|
||||
|
||||
for (int il = 0; il < n_layer; ++il) {
|
||||
ggml_tensor * inpSA = inpL;
|
||||
|
||||
// self-attention
|
||||
{
|
||||
cur = build_norm(inpL, model.layers[il].attn_norm, NULL, LLM_NORM_RMS, il);
|
||||
cb(cur, "attn_norm", il);
|
||||
|
||||
auto [Qcur, Kcur, Vcur] = build_qkv(model.layers[il], cur,
|
||||
n_embd_head, n_head, n_head_kv, il);
|
||||
|
||||
// per-head QK RMSNorm (weights already include Gemma's +1)
|
||||
Qcur = build_norm(Qcur, model.layers[il].attn_q_norm, NULL, LLM_NORM_RMS, il);
|
||||
cb(Qcur, "Qcur_normed", il);
|
||||
Kcur = build_norm(Kcur, model.layers[il].attn_k_norm, NULL, LLM_NORM_RMS, il);
|
||||
cb(Kcur, "Kcur_normed", il);
|
||||
|
||||
// partial rotary: only the first n_rot dims are rotated
|
||||
Qcur = ggml_rope_ext(
|
||||
ctx0, Qcur, inp_pos, nullptr,
|
||||
n_rot, rope_type, n_ctx_orig, freq_base, freq_scale,
|
||||
ext_factor, attn_factor, beta_fast, beta_slow);
|
||||
Kcur = ggml_rope_ext(
|
||||
ctx0, Kcur, inp_pos, nullptr,
|
||||
n_rot, rope_type, n_ctx_orig, freq_base, freq_scale,
|
||||
ext_factor, attn_factor, beta_fast, beta_slow);
|
||||
|
||||
cb(Qcur, "Qcur", il);
|
||||
cb(Kcur, "Kcur", il);
|
||||
cb(Vcur, "Vcur", il);
|
||||
|
||||
const bool is_sparse = msa_enabled && il >= (int) hparams.n_layer_dense_lead;
|
||||
|
||||
if (!is_sparse) {
|
||||
cur = build_attn(inp_attn, model.layers[il].wo, NULL, model.layers[il].wo_s,
|
||||
Qcur, Kcur, Vcur, nullptr, nullptr, nullptr,
|
||||
1.0f/sqrtf(float(n_embd_head)), il);
|
||||
} else {
|
||||
const int64_t n_idx_dim = hparams.indexer_head_size; // 128
|
||||
|
||||
GGML_ASSERT(!inp_attn->self_k_rot && !inp_attn->self_v_rot && "MSA: attn-rot not supported");
|
||||
|
||||
// Index Branch, project, norm, partial RoPE, cache
|
||||
ggml_tensor * iq = build_lora_mm(model.layers[il].index_q_proj, cur);
|
||||
ggml_tensor * ik = build_lora_mm(model.layers[il].index_k_proj, cur);
|
||||
iq = ggml_reshape_3d(ctx0, iq, n_idx_dim, Hd, n_tokens);
|
||||
ik = ggml_reshape_3d(ctx0, ik, n_idx_dim, 1, n_tokens);
|
||||
iq = build_norm(iq, model.layers[il].index_q_norm, NULL, LLM_NORM_RMS, il); // +1 baked
|
||||
ik = build_norm(ik, model.layers[il].index_k_norm, NULL, LLM_NORM_RMS, il);
|
||||
iq = ggml_rope_ext(ctx0, iq, inp_pos, nullptr, n_rot, rope_type, n_ctx_orig,
|
||||
freq_base, freq_scale, ext_factor, attn_factor, beta_fast, beta_slow);
|
||||
ik = ggml_rope_ext(ctx0, ik, inp_pos, nullptr, n_rot, rope_type, n_ctx_orig,
|
||||
freq_base, freq_scale, ext_factor, attn_factor, beta_fast, beta_slow);
|
||||
|
||||
const auto * mctx_cur = inp_attn->mctx;
|
||||
ggml_build_forward_expand(gf, mctx_cur->cpy_k_idx(ctx0, ik, inp_attn->get_k_idxs(), il));
|
||||
ggml_tensor * ik_kv = mctx_cur->get_k_idx(ctx0, il);
|
||||
|
||||
// Main branch: store K/V, take cache views
|
||||
ggml_build_forward_expand(gf, Qcur);
|
||||
ggml_build_forward_expand(gf, Kcur);
|
||||
ggml_build_forward_expand(gf, Vcur);
|
||||
ggml_build_forward_expand(gf, mctx_cur->cpy_k(ctx0, Kcur, inp_attn->get_k_idxs(), il));
|
||||
ggml_build_forward_expand(gf, mctx_cur->cpy_v(ctx0, Vcur, inp_attn->get_v_idxs(), il));
|
||||
ggml_tensor * k = mctx_cur->get_k(ctx0, il);
|
||||
ggml_tensor * v = mctx_cur->get_v(ctx0, il);
|
||||
GGML_ASSERT(!(v->nb[1] > v->nb[2]) && "MSA assumes v_trans=false (FA on)");
|
||||
|
||||
const int64_t D = k->ne[0];
|
||||
const int64_t HKV = k->ne[1];
|
||||
const int64_t Gp = n_head/HKV;
|
||||
GGML_ASSERT(HKV == Hd && "MSA: one indexer head per GQA group");
|
||||
GGML_ASSERT(k->ne[3] == ns);
|
||||
const int K = mm.msa_p.topk_blocks < (int) nblk ? mm.msa_p.topk_blocks : (int) nblk;
|
||||
|
||||
const float kq_scale = 1.0f/sqrtf(float(n_embd_head));
|
||||
|
||||
if (msa_decode) {
|
||||
// decode: batched over streams top-k + gather, one grouped FA
|
||||
// scores: per-stream batched matmul over the stream dim (ne[3]).
|
||||
// the cache views are not contiguous across streams (stride = kv_size, not n_kv)
|
||||
ggml_tensor * ikv4 = ggml_view_4d(ctx0, ik_kv, n_idx_dim, n_kv, 1, ns,
|
||||
ik_kv->nb[2], ik_kv->nb[3], ik_kv->nb[3], 0);
|
||||
ggml_tensor * iq4 = ggml_reshape_4d(ctx0, iq, n_idx_dim, Hd, 1, ns);
|
||||
ggml_tensor * sc = ggml_mul_mat(ctx0, ikv4, iq4);
|
||||
ggml_mul_mat_set_prec(sc, GGML_PREC_F32);
|
||||
sc = ggml_add_inplace(ctx0, sc, msa_mf);
|
||||
ggml_tensor * bs = ggml_pool_2d(ctx0, sc, GGML_OP_POOL_MAX, blk, 1, blk, 1, 0, 0);
|
||||
cb(bs, "msa_bs", il);
|
||||
|
||||
ggml_tensor * bsf = ggml_add(ctx0, bs,
|
||||
ggml_reshape_4d(ctx0, msa_loc->bias, nblk, 1, 1, ns));
|
||||
ggml_tensor * idx = ggml_top_k(ctx0, bsf, K);
|
||||
|
||||
// token idx: tj[t,k,h,s] = blk*idx[k,h,s] + t (for the mask gather)
|
||||
// row idx: tr[t,k,h,s] = tj*HKV + h (for the per-stream K/V gather)
|
||||
ggml_tensor * a = ggml_scale(ctx0, ggml_cast(ctx0, idx, GGML_TYPE_F32), (float) blk);
|
||||
a = ggml_reshape_4d(ctx0, a, 1, K, Hd, ns);
|
||||
ggml_tensor * tj = ggml_add(ctx0,
|
||||
ggml_repeat_4d(ctx0, a, blk, K, Hd, ns),
|
||||
ggml_reshape_3d(ctx0, ggml_arange(ctx0, 0.0f, (float) blk, 1.0f), blk, 1, 1));
|
||||
ggml_tensor * tr = ggml_add(ctx0,
|
||||
ggml_scale(ctx0, tj, (float) HKV),
|
||||
ggml_reshape_3d(ctx0, ggml_arange(ctx0, 0.0f, (float) HKV, 1.0f), 1, 1, Hd));
|
||||
|
||||
ggml_tensor * tokj = ggml_cast(ctx0, ggml_reshape_2d(ctx0, tj, (int64_t) blk*K*Hd, ns), GGML_TYPE_I32);
|
||||
ggml_tensor * tokr = ggml_cast(ctx0, ggml_reshape_2d(ctx0, tr, (int64_t) blk*K*Hd, ns), GGML_TYPE_I32);
|
||||
|
||||
ggml_tensor * k3 = ggml_view_3d(ctx0, k, D, HKV*n_kv, ns, k->nb[1], k->nb[3], 0);
|
||||
ggml_tensor * v3 = ggml_view_3d(ctx0, v, D, HKV*n_kv, ns, v->nb[1], v->nb[3], 0);
|
||||
ggml_tensor * m3 = ggml_reshape_3d(ctx0, msa_kqm, 1, n_kv, ns);
|
||||
|
||||
ggml_tensor * kg = ggml_get_rows(ctx0, k3, tokr);
|
||||
ggml_tensor * vg = ggml_get_rows(ctx0, v3, tokr);
|
||||
ggml_tensor * mg = ggml_get_rows(ctx0, m3, tokj);
|
||||
|
||||
// fold (group, stream) onto the FA channel dim
|
||||
const ggml_type kt = ggml_is_quantized(k->type) ? GGML_TYPE_F16 : k->type;
|
||||
const ggml_type vt = ggml_is_quantized(v->type) ? GGML_TYPE_F16 : v->type;
|
||||
ggml_tensor * kfa = ggml_reshape_4d(ctx0, kg, D, (int64_t) blk*K, 1, Hd*ns);
|
||||
ggml_tensor * vfa = ggml_reshape_4d(ctx0, vg, D, (int64_t) blk*K, 1, Hd*ns);
|
||||
if (kfa->type != kt) { kfa = ggml_cast(ctx0, kfa, kt); }
|
||||
if (vfa->type != vt) { vfa = ggml_cast(ctx0, vfa, vt); }
|
||||
// the FA mask must be F16
|
||||
ggml_tensor * mfa = ggml_cast(ctx0, ggml_reshape_4d(ctx0, mg, (int64_t) blk*K, 1, 1, Hd*ns), GGML_TYPE_F16);
|
||||
|
||||
cur = build_attn_msa_fa(Qcur, kfa, vfa, mfa, Gp, kq_scale, il);
|
||||
} else {
|
||||
// batch: per-stream loop
|
||||
std::vector<ggml_tensor *> outs(ns);
|
||||
for (int64_t st = 0; st < ns; ++st) {
|
||||
ggml_tensor * iq_s = ggml_view_3d(ctx0, iq, n_idx_dim, Hd, n_tps,
|
||||
iq->nb[1], iq->nb[2], st*n_tps*iq->nb[2]);
|
||||
ggml_tensor * ik_s = ggml_view_2d(ctx0, ik_kv, n_idx_dim, n_kv,
|
||||
ik_kv->nb[2], st*ik_kv->nb[3]);
|
||||
ggml_tensor * mf_s = ggml_view_3d(ctx0, msa_mf, n_kv, 1, n_tps,
|
||||
msa_mf->nb[1], msa_mf->nb[1], st*msa_mf->nb[3]);
|
||||
ggml_tensor * km_s = ggml_view_3d(ctx0, msa_kqm, n_kv, n_tps, 1,
|
||||
msa_kqm->nb[1], msa_kqm->nb[3], st*msa_kqm->nb[3]);
|
||||
ggml_tensor * bias_s = ggml_view_2d(ctx0, msa_loc->bias, nblk, n_tps,
|
||||
msa_loc->bias->nb[1], st*n_tps*msa_loc->bias->nb[1]);
|
||||
ggml_tensor * q_s = ggml_view_3d(ctx0, Qcur, D, n_head, n_tps,
|
||||
Qcur->nb[1], Qcur->nb[2], st*n_tps*Qcur->nb[2]);
|
||||
ggml_tensor * k_s = ggml_view_4d(ctx0, k, D, HKV, n_kv, 1,
|
||||
k->nb[1], k->nb[2], k->nb[3], st*k->nb[3]);
|
||||
ggml_tensor * v_s = ggml_view_4d(ctx0, v, D, HKV, n_kv, 1,
|
||||
v->nb[1], v->nb[2], v->nb[3], st*v->nb[3]);
|
||||
|
||||
// block scores: bs = maxpool_blk(idx_q * idx_k^T + causal mask)
|
||||
// scores are unscaled, only the top-k ordering matters
|
||||
ggml_tensor * sc = ggml_mul_mat(ctx0, ik_s,
|
||||
ggml_reshape_2d(ctx0, iq_s, n_idx_dim, Hd*n_tps));
|
||||
// indexer scores run in F32
|
||||
ggml_mul_mat_set_prec(sc, GGML_PREC_F32);
|
||||
sc = ggml_reshape_3d(ctx0, sc, n_kv, Hd, n_tps);
|
||||
sc = ggml_add_inplace(ctx0, sc, mf_s);
|
||||
ggml_tensor * bs = ggml_pool_2d(ctx0, sc, GGML_OP_POOL_MAX, blk, 1, blk, 1, 0, 0);
|
||||
cb(bs, "msa_bs", il);
|
||||
|
||||
// block-level 0/-inf keep mask on the CPU, tiny transfer
|
||||
ggml_tensor * srcs[2] = { bs, bias_s };
|
||||
ggml_tensor * bm = ggml_custom_4d(ctx0, GGML_TYPE_F16,
|
||||
nblk, n_tps, Hd, 1,
|
||||
srcs, 2, msa_block_mask_op, GGML_N_TASKS_MAX,
|
||||
const_cast<msa_params *>(&mm.msa_p));
|
||||
cb(bm, "msa_block_mask", il);
|
||||
|
||||
// expand block -> token granularity on the GPU (j = bk*blk + t),
|
||||
// then combine with the causal mask in place
|
||||
ggml_tensor * bmx = ggml_repeat_4d(ctx0,
|
||||
ggml_reshape_3d(ctx0, bm, 1, nblk, n_tps*Hd),
|
||||
blk, nblk, n_tps*Hd, 1);
|
||||
bmx = ggml_reshape_3d(ctx0, bmx, n_kv, n_tps, Hd);
|
||||
ggml_tensor * mask4 = ggml_add_inplace(ctx0, bmx, km_s);
|
||||
mask4 = ggml_reshape_4d(ctx0, mask4, n_kv, n_tps, 1, Hd);
|
||||
cb(mask4, "msa_mask4", il);
|
||||
|
||||
// cache views with groups on ne[3];
|
||||
ggml_tensor * kfa = ggml_permute(ctx0, k_s, 0, 3, 1, 2);
|
||||
ggml_tensor * vfa = ggml_permute(ctx0, v_s, 0, 3, 1, 2);
|
||||
|
||||
outs[st] = build_attn_msa_fa(q_s, kfa, vfa, mask4, Gp, kq_scale, il);
|
||||
}
|
||||
cur = outs[0];
|
||||
for (int64_t st = 1; st < ns; ++st) {
|
||||
cur = ggml_concat(ctx0, cur, outs[st], 1);
|
||||
}
|
||||
}
|
||||
|
||||
cb(cur, "kqv_out", il);
|
||||
if (model.layers[il].wo) {
|
||||
cur = build_lora_mm(model.layers[il].wo, cur, model.layers[il].wo_s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (il == n_layer - 1 && inp_out_ids) {
|
||||
cur = ggml_get_rows(ctx0, cur, inp_out_ids);
|
||||
inpSA = ggml_get_rows(ctx0, inpSA, inp_out_ids);
|
||||
}
|
||||
|
||||
ggml_tensor * ffn_inp = ggml_add(ctx0, cur, inpSA);
|
||||
cb(ffn_inp, "ffn_inp", il);
|
||||
|
||||
cur = build_norm(ffn_inp, model.layers[il].ffn_norm, NULL, LLM_NORM_RMS, il);
|
||||
cb(cur, "ffn_norm", il);
|
||||
|
||||
if ((uint32_t) il < hparams.n_layer_dense_lead) {
|
||||
// leading dense FFN (swigluoai)
|
||||
cur = build_ffn(cur,
|
||||
model.layers[il].ffn_up, NULL, NULL,
|
||||
model.layers[il].ffn_gate, NULL, NULL,
|
||||
model.layers[il].ffn_down, NULL, NULL,
|
||||
NULL,
|
||||
LLM_FFN_SWIGLU_OAI_MOE, LLM_FFN_PAR, il);
|
||||
cb(cur, "ffn_out", il);
|
||||
} else {
|
||||
// routed experts (swigluoai MoE)
|
||||
ggml_tensor * moe_out = build_moe_ffn(cur,
|
||||
model.layers[il].ffn_gate_inp,
|
||||
model.layers[il].ffn_up_exps,
|
||||
model.layers[il].ffn_gate_exps,
|
||||
model.layers[il].ffn_down_exps,
|
||||
model.layers[il].ffn_exp_probs_b,
|
||||
n_expert, n_expert_used,
|
||||
LLM_FFN_SWIGLU_OAI_MOE, hparams.expert_weights_norm,
|
||||
hparams.expert_weights_scale,
|
||||
(llama_expert_gating_func_type) hparams.expert_gating_func,
|
||||
il);
|
||||
cb(moe_out, "ffn_moe_out", il);
|
||||
|
||||
// shared expert (swigluoai)
|
||||
ggml_tensor * ffn_shexp = build_ffn(cur,
|
||||
model.layers[il].ffn_up_shexp, NULL, NULL,
|
||||
model.layers[il].ffn_gate_shexp, NULL, NULL,
|
||||
model.layers[il].ffn_down_shexp, NULL, NULL,
|
||||
NULL,
|
||||
LLM_FFN_SWIGLU_OAI_MOE, LLM_FFN_PAR, il);
|
||||
cb(ffn_shexp, "ffn_shexp", il);
|
||||
|
||||
cur = ggml_add(ctx0, moe_out, ffn_shexp);
|
||||
cb(cur, "ffn_out", il);
|
||||
}
|
||||
|
||||
cur = ggml_add(ctx0, cur, ffn_inp);
|
||||
|
||||
cur = build_cvec(cur, il);
|
||||
cb(cur, "l_out", il);
|
||||
|
||||
// input for next layer
|
||||
inpL = cur;
|
||||
}
|
||||
|
||||
cur = inpL;
|
||||
|
||||
cur = build_norm(cur, model.output_norm, NULL, LLM_NORM_RMS, -1);
|
||||
cb(cur, "result_norm", -1);
|
||||
res->t_embd = cur;
|
||||
|
||||
// lm_head
|
||||
cur = build_lora_mm(model.output, cur, model.output_s);
|
||||
cb(cur, "result_output", -1);
|
||||
res->t_logits = cur;
|
||||
|
||||
ggml_build_forward_expand(gf, cur);
|
||||
}
|
||||
@@ -424,6 +424,22 @@ struct llama_model_mellum : public llama_model_base {
|
||||
std::unique_ptr<llm_graph_context> build_arch_graph(const llm_graph_params & params) const override;
|
||||
};
|
||||
|
||||
struct llama_model_nanbeige : public llama_model_base {
|
||||
llama_model_nanbeige(const struct llama_model_params & params) : llama_model_base(params) {}
|
||||
void load_arch_hparams(llama_model_loader & ml) override;
|
||||
void load_arch_tensors(llama_model_loader & ml) override;
|
||||
|
||||
int n_loops = 1;
|
||||
int n_layer_phys = 0;
|
||||
bool skip_loop_final_norm = false;
|
||||
|
||||
struct graph : public llm_graph_context {
|
||||
graph(const llama_model & model, const llm_graph_params & params);
|
||||
};
|
||||
|
||||
std::unique_ptr<llm_graph_context> build_arch_graph(const llm_graph_params & params) const override;
|
||||
};
|
||||
|
||||
struct llama_model_qwen : public llama_model_base {
|
||||
llama_model_qwen(const struct llama_model_params & params) : llama_model_base(params) {}
|
||||
void load_arch_hparams(llama_model_loader & ml) override;
|
||||
@@ -1902,6 +1918,29 @@ struct llama_model_minimax_m2 : public llama_model_base {
|
||||
std::unique_ptr<llm_graph_context> build_arch_graph(const llm_graph_params & params) const override;
|
||||
};
|
||||
|
||||
struct msa_params {
|
||||
int blk;
|
||||
int topk_blocks;
|
||||
int local;
|
||||
};
|
||||
|
||||
struct llama_model_minimax_m3 : public llama_model_base {
|
||||
llama_model_minimax_m3(const struct llama_model_params & params) : llama_model_base(params) {}
|
||||
void load_arch_hparams(llama_model_loader & ml) override;
|
||||
void load_arch_tensors(llama_model_loader & ml) override;
|
||||
msa_params msa_p;
|
||||
struct graph : public llm_graph_context {
|
||||
graph(const llama_model & model, const llm_graph_params & params);
|
||||
|
||||
ggml_tensor * build_attn_msa_fa(
|
||||
ggml_tensor * q_cur, // [D, HQ, S] f32
|
||||
ggml_tensor * k, // [D, n_keys, 1, C] C = HKV or HKV*n_stream
|
||||
ggml_tensor * v, // [D, n_keys, 1, C]
|
||||
ggml_tensor * mask, // [n_keys, R, 1, C] f16, R = HQ*T/(Gp*C)
|
||||
int64_t Gp, float kq_scale, int il) const;
|
||||
};
|
||||
std::unique_ptr<llm_graph_context> build_arch_graph(const llm_graph_params & params) const override;
|
||||
};
|
||||
|
||||
struct llama_model_cogvlm : public llama_model_base {
|
||||
llama_model_cogvlm(const struct llama_model_params & params) : llama_model_base(params) {}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
#include "models.h"
|
||||
|
||||
void llama_model_nanbeige::load_arch_hparams(llama_model_loader & ml) {
|
||||
ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps);
|
||||
|
||||
uint32_t n_loops_u = 1;
|
||||
ml.get_key(LLM_KV_NUM_LOOPS, n_loops_u, false);
|
||||
GGML_ASSERT(n_loops_u >= 1);
|
||||
|
||||
skip_loop_final_norm = false;
|
||||
ml.get_key(LLM_KV_SKIP_LOOP_FINAL_NORM, skip_loop_final_norm, false);
|
||||
|
||||
n_layer_phys = (int) hparams.n_layer();
|
||||
|
||||
// Bound-check before casting: signed int mul can overflow and bypass the guard.
|
||||
GGML_ASSERT((size_t) n_layer_phys * (size_t) n_loops_u <= (size_t) LLAMA_MAX_LAYERS);
|
||||
n_loops = (int) n_loops_u;
|
||||
|
||||
// Expand logical layer count before load_tensors() allocates layers / KV.
|
||||
if (n_loops > 1) {
|
||||
for (int j = 1; j < n_loops; ++j) {
|
||||
for (int i = 0; i < n_layer_phys; ++i) {
|
||||
const int dst = i + j * n_layer_phys;
|
||||
hparams.n_head_arr[dst] = hparams.n_head_arr[i];
|
||||
hparams.n_head_kv_arr[dst] = hparams.n_head_kv_arr[i];
|
||||
hparams.n_ff_arr[dst] = hparams.n_ff_arr[i];
|
||||
hparams.is_swa_impl[dst] = hparams.is_swa_impl[i];
|
||||
hparams.is_recr_impl[dst] = hparams.is_recr_impl[i];
|
||||
}
|
||||
}
|
||||
hparams.n_layer_all = (uint32_t) ((size_t) n_layer_phys * (size_t) n_loops);
|
||||
}
|
||||
|
||||
type = LLM_TYPE_UNKNOWN;
|
||||
}
|
||||
|
||||
void llama_model_nanbeige::load_arch_tensors(llama_model_loader &) {
|
||||
LLAMA_LOAD_LOCALS;
|
||||
|
||||
tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0);
|
||||
|
||||
output_norm = create_tensor(tn(LLM_TENSOR_OUTPUT_NORM, "weight"), {n_embd}, 0);
|
||||
output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), {n_embd, n_vocab}, TENSOR_NOT_REQUIRED);
|
||||
if (output == NULL) {
|
||||
output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, TENSOR_DUPLICATED);
|
||||
}
|
||||
|
||||
const int n_phys = n_layer_phys > 0 ? n_layer_phys : n_layer;
|
||||
for (int i = 0; i < n_phys; ++i) {
|
||||
auto & layer = layers[i];
|
||||
|
||||
layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0);
|
||||
|
||||
create_tensor_qkv(layer, i, n_embd, n_embd_head_k * n_head, n_embd_k_gqa, n_embd_v_gqa, 0);
|
||||
layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_embd_head_k * n_head, n_embd}, 0);
|
||||
|
||||
layer.rope_freqs = create_tensor(tn(LLM_TENSOR_ROPE_FREQS, "weight", i), {n_rot/2},
|
||||
TENSOR_NOT_REQUIRED | (i != 0 ? TENSOR_DUPLICATED : 0));
|
||||
|
||||
layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, 0);
|
||||
layer.ffn_gate = create_tensor(tn(LLM_TENSOR_FFN_GATE, "weight", i), {n_embd, n_ff}, 0);
|
||||
layer.ffn_down = create_tensor(tn(LLM_TENSOR_FFN_DOWN, "weight", i), { n_ff, n_embd}, 0);
|
||||
layer.ffn_up = create_tensor(tn(LLM_TENSOR_FFN_UP, "weight", i), {n_embd, n_ff}, 0);
|
||||
}
|
||||
|
||||
// Share physical weights across loops; each slot still has its own KV index.
|
||||
if (n_loops > 1) {
|
||||
for (int j = 1; j < n_loops; ++j) {
|
||||
for (int i = 0; i < n_phys; ++i) {
|
||||
layers[i + j * n_phys] = layers[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<llm_graph_context> llama_model_nanbeige::build_arch_graph(const llm_graph_params & params) const {
|
||||
return std::make_unique<graph>(*this, params);
|
||||
}
|
||||
|
||||
llama_model_nanbeige::graph::graph(const llama_model & model, const llm_graph_params & params) :
|
||||
llm_graph_context(params) {
|
||||
const auto & nb = static_cast<const llama_model_nanbeige &>(model);
|
||||
|
||||
const int64_t n_embd_head = hparams.n_embd_head_v();
|
||||
GGML_ASSERT(n_embd_head == hparams.n_embd_head_k());
|
||||
|
||||
const int n_phys = nb.n_layer_phys > 0 ? nb.n_layer_phys : (int) n_layer;
|
||||
const int n_loops = nb.n_loops > 0 ? nb.n_loops : 1;
|
||||
|
||||
ggml_tensor * cur;
|
||||
ggml_tensor * inpL;
|
||||
|
||||
inpL = build_inp_embd(model.tok_embd);
|
||||
|
||||
ggml_tensor * inp_pos = build_inp_pos();
|
||||
|
||||
auto * inp_attn = build_attn_inp_kv();
|
||||
|
||||
const float kq_scale = hparams.f_attention_scale == 0.0f
|
||||
? 1.0f / sqrtf(float(n_embd_head))
|
||||
: hparams.f_attention_scale;
|
||||
|
||||
ggml_tensor * inp_out_ids = build_inp_out_ids();
|
||||
|
||||
for (int il = 0; il < n_layer; ++il) {
|
||||
ggml_tensor * inpSA = inpL;
|
||||
|
||||
cur = build_norm(inpL, model.layers[il].attn_norm, NULL, LLM_NORM_RMS, il);
|
||||
cb(cur, "attn_norm", il);
|
||||
|
||||
{
|
||||
ggml_tensor * rope_factors = model.get_rope_factors(cparams, il);
|
||||
|
||||
auto [Qcur, Kcur, Vcur] = build_qkv(model.layers[il], cur,
|
||||
n_embd_head, n_head, n_head_kv, il);
|
||||
|
||||
Qcur = ggml_rope_ext(
|
||||
ctx0, Qcur, inp_pos, rope_factors,
|
||||
n_rot, rope_type, n_ctx_orig, freq_base, freq_scale,
|
||||
ext_factor, attn_factor, beta_fast, beta_slow);
|
||||
|
||||
Kcur = ggml_rope_ext(
|
||||
ctx0, Kcur, inp_pos, rope_factors,
|
||||
n_rot, rope_type, n_ctx_orig, freq_base, freq_scale,
|
||||
ext_factor, attn_factor, beta_fast, beta_slow);
|
||||
|
||||
cb(Qcur, "Qcur", il);
|
||||
cb(Kcur, "Kcur", il);
|
||||
cb(Vcur, "Vcur", il);
|
||||
|
||||
cur = build_attn(inp_attn,
|
||||
model.layers[il].wo, model.layers[il].wo_b, model.layers[il].wo_s,
|
||||
Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, kq_scale, il);
|
||||
cb(cur, "attn_out", il);
|
||||
}
|
||||
|
||||
if (il == n_layer - 1 && inp_out_ids) {
|
||||
cur = ggml_get_rows(ctx0, cur, inp_out_ids);
|
||||
inpSA = ggml_get_rows(ctx0, inpSA, inp_out_ids);
|
||||
}
|
||||
|
||||
ggml_tensor * ffn_inp = ggml_add(ctx0, cur, inpSA);
|
||||
cb(ffn_inp, "ffn_inp", il);
|
||||
|
||||
cur = build_norm(ffn_inp, model.layers[il].ffn_norm, NULL, LLM_NORM_RMS, il);
|
||||
cb(cur, "ffn_norm", il);
|
||||
|
||||
cur = build_ffn(cur,
|
||||
model.layers[il].ffn_up, model.layers[il].ffn_up_b, model.layers[il].ffn_up_s,
|
||||
model.layers[il].ffn_gate, model.layers[il].ffn_gate_b, model.layers[il].ffn_gate_s,
|
||||
model.layers[il].ffn_down, model.layers[il].ffn_down_b, model.layers[il].ffn_down_s,
|
||||
NULL, LLM_FFN_SILU, LLM_FFN_PAR, il);
|
||||
cb(cur, "ffn_out", il);
|
||||
|
||||
cur = ggml_add(ctx0, cur, ffn_inp);
|
||||
cb(cur, "ffn_out", il);
|
||||
|
||||
cur = build_cvec(cur, il);
|
||||
cb(cur, "l_out", il);
|
||||
|
||||
inpL = cur;
|
||||
|
||||
if (n_loops > 1 &&
|
||||
((il + 1) % n_phys) == 0 &&
|
||||
(il + 1) < n_layer &&
|
||||
!nb.skip_loop_final_norm) {
|
||||
cur = build_norm(inpL, model.output_norm, NULL, LLM_NORM_RMS, il);
|
||||
cb(cur, "loop_norm", il);
|
||||
inpL = cur;
|
||||
}
|
||||
}
|
||||
|
||||
cur = inpL;
|
||||
|
||||
cur = build_norm(cur, model.output_norm, NULL, LLM_NORM_RMS, -1);
|
||||
cb(cur, "result_norm", -1);
|
||||
res->t_embd = cur;
|
||||
|
||||
cur = build_lora_mm(model.output, cur, model.output_s);
|
||||
cb(cur, "result_output", -1);
|
||||
res->t_logits = cur;
|
||||
|
||||
ggml_build_forward_expand(gf, cur);
|
||||
}
|
||||
@@ -116,7 +116,7 @@ llama_model_openai_moe::graph::graph(const llama_model & model, const llm_graph_
|
||||
|
||||
cb(cur, "attn_out", il);
|
||||
}
|
||||
if (il == n_layer - 1) {
|
||||
if (il == n_layer - 1 && inp_out_ids && cparams.embeddings_nextn_masked) {
|
||||
// skip computing output for unused tokens
|
||||
cur = ggml_get_rows(ctx0, cur, inp_out_ids);
|
||||
inpSA = ggml_get_rows(ctx0, inpSA, inp_out_ids);
|
||||
@@ -154,6 +154,12 @@ llama_model_openai_moe::graph::graph(const llama_model & model, const llm_graph_
|
||||
}
|
||||
cur = inpL;
|
||||
|
||||
res->t_h_nextn = cur;
|
||||
|
||||
if (!cparams.embeddings_nextn_masked && inp_out_ids) {
|
||||
cur = ggml_get_rows(ctx0, cur, inp_out_ids);
|
||||
}
|
||||
|
||||
cur = build_norm(cur,
|
||||
model.output_norm, NULL,
|
||||
LLM_NORM_RMS, -1);
|
||||
|
||||
@@ -143,6 +143,10 @@ static void test(void) {
|
||||
assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON));
|
||||
assert(params.load_mode == LLAMA_LOAD_MODE_MLOCK);
|
||||
|
||||
argv = {"binary_name", "-lm", "mmap+mlock"};
|
||||
assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON));
|
||||
assert(params.load_mode == LLAMA_LOAD_MODE_MMAP_MLOCK);
|
||||
|
||||
argv = {"binary_name", "-lm", "dio"};
|
||||
assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON));
|
||||
assert(params.load_mode == LLAMA_LOAD_MODE_DIRECT_IO);
|
||||
@@ -187,6 +191,11 @@ static void test(void) {
|
||||
assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON));
|
||||
assert(params.load_mode == LLAMA_LOAD_MODE_MLOCK);
|
||||
|
||||
setenv("LLAMA_ARG_LOAD_MODE", "mmap+mlock", true);
|
||||
argv = {"binary_name"};
|
||||
assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON));
|
||||
assert(params.load_mode == LLAMA_LOAD_MODE_MMAP_MLOCK);
|
||||
|
||||
setenv("LLAMA_ARG_LOAD_MODE", "dio", true);
|
||||
argv = {"binary_name"};
|
||||
assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON));
|
||||
|
||||
+11
-14
@@ -4,7 +4,7 @@
|
||||
#include <cstdlib>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <sheredom/subprocess.h>
|
||||
#include "subproc.h"
|
||||
|
||||
#include "jinja/runtime.h"
|
||||
#include "jinja/parser.h"
|
||||
@@ -2135,21 +2135,20 @@ static void test_template_py(testing & t, const std::string & name, const std::s
|
||||
const char * python_executable = "python3";
|
||||
#endif
|
||||
|
||||
const char * command_line[] = {python_executable, "-c", py_script.c_str(), NULL};
|
||||
std::vector<std::string> args = {python_executable, "-c", py_script, };
|
||||
|
||||
struct subprocess_s subprocess;
|
||||
common_subproc subprocess;
|
||||
int options = subprocess_option_combined_stdout_stderr
|
||||
| subprocess_option_no_window
|
||||
| subprocess_option_inherit_environment
|
||||
| subprocess_option_search_user_path;
|
||||
int result = subprocess_create(command_line, options, &subprocess);
|
||||
|
||||
if (result != 0) {
|
||||
t.log("Failed to create subprocess, error code: " + std::to_string(result));
|
||||
if (!subprocess.create(args, options)) {
|
||||
t.log("Failed to create subprocess");
|
||||
t.assert_true("subprocess creation", false);
|
||||
return;
|
||||
}
|
||||
FILE * p_stdin = subprocess_stdin(&subprocess);
|
||||
FILE * p_stdin = subprocess.stdin_file();
|
||||
|
||||
// Write input
|
||||
std::string input = merged.dump();
|
||||
@@ -2157,24 +2156,22 @@ static void test_template_py(testing & t, const std::string & name, const std::s
|
||||
if (written != input.size()) {
|
||||
t.log("Failed to write complete input to subprocess stdin");
|
||||
t.assert_true("subprocess stdin write", false);
|
||||
subprocess_destroy(&subprocess);
|
||||
subprocess.close_stdin();
|
||||
subprocess.join();
|
||||
return;
|
||||
}
|
||||
fflush(p_stdin);
|
||||
fclose(p_stdin); // Close stdin to signal EOF to the Python process
|
||||
subprocess.stdin_file = nullptr;
|
||||
subprocess.close_stdin(); // Close stdin to signal EOF to the Python process
|
||||
|
||||
// Read output
|
||||
std::string output;
|
||||
char buffer[1024];
|
||||
FILE * p_stdout = subprocess_stdout(&subprocess);
|
||||
FILE * p_stdout = subprocess.stdout_file();
|
||||
while (fgets(buffer, sizeof(buffer), p_stdout)) {
|
||||
output += buffer;
|
||||
}
|
||||
|
||||
int process_return;
|
||||
subprocess_join(&subprocess, &process_return);
|
||||
subprocess_destroy(&subprocess);
|
||||
int process_return = subprocess.join();
|
||||
|
||||
if (process_return != 0) {
|
||||
t.log("Python script failed with exit code: " + std::to_string(process_return));
|
||||
|
||||
@@ -168,6 +168,9 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
|
||||
ms.add_kv(LLM_KV_ROPE_DIMENSION_COUNT, uint32_t(64));
|
||||
ms.add_kv(LLM_KV_ATTENTION_KEY_LENGTH_MLA, uint32_t(192));
|
||||
ms.add_kv(LLM_KV_ATTENTION_VALUE_LENGTH_MLA, uint32_t(128));
|
||||
} else if (arch == LLM_ARCH_MINIMAX_M3) {
|
||||
// partial rotary: n_rot must not exceed the indexer key length (64)
|
||||
ms.add_kv(LLM_KV_ROPE_DIMENSION_COUNT, uint32_t(64));
|
||||
}
|
||||
ms.add_kv(LLM_KV_ATTENTION_CLAMP_KQV, 1.0f);
|
||||
ms.add_kv(LLM_KV_ATTENTION_LAYERNORM_EPS, 1e-5f);
|
||||
@@ -198,9 +201,13 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
|
||||
ms.add_kv(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, uint32_t(2));
|
||||
}
|
||||
|
||||
ms.add_kv(LLM_KV_ATTENTION_INDEXER_HEAD_COUNT, uint32_t(1));
|
||||
ms.add_kv(LLM_KV_ATTENTION_INDEXER_KEY_LENGTH, uint32_t(64));
|
||||
ms.add_kv(LLM_KV_ATTENTION_INDEXER_TOP_K, uint32_t(8));
|
||||
// MSA requires one indexer head per GQA (KV) head, unlike the DSA archs where the
|
||||
// indexer head count is independent of the main attention head count.
|
||||
ms.add_kv(LLM_KV_ATTENTION_INDEXER_HEAD_COUNT, arch == LLM_ARCH_MINIMAX_M3 ? n_head : uint32_t(1));
|
||||
ms.add_kv(LLM_KV_ATTENTION_INDEXER_KEY_LENGTH, uint32_t(64));
|
||||
ms.add_kv(LLM_KV_ATTENTION_INDEXER_TOP_K, uint32_t(8));
|
||||
ms.add_kv(LLM_KV_ATTENTION_INDEXER_BLOCK_SIZE, uint32_t(4));
|
||||
ms.add_kv(LLM_KV_ATTENTION_INDEXER_LOCAL_BLOCKS, uint32_t(1));
|
||||
ms.add_kv(LLM_KV_ROPE_DIMENSION_SECTIONS, std::vector<uint32_t>({n_embd_head/4, n_embd_head/4, n_embd_head/4, n_embd_head/4}));
|
||||
ms.add_kv(LLM_KV_TOKENIZER_MODEL, "no_vocab");
|
||||
// ms.add_kv(LLM_KV_DENSE_2_FEAT_OUT, n_embd);
|
||||
@@ -355,6 +362,7 @@ static bool moe_mandatory(const llm_arch arch) {
|
||||
case LLM_ARCH_LLADA_MOE:
|
||||
case LLM_ARCH_GROVEMOE:
|
||||
case LLM_ARCH_MINIMAX_M2:
|
||||
case LLM_ARCH_MINIMAX_M3:
|
||||
case LLM_ARCH_RND1:
|
||||
case LLM_ARCH_PADDLEOCR:
|
||||
case LLM_ARCH_MIMO2:
|
||||
|
||||
@@ -44,8 +44,6 @@ static llama_tokens generate_tokens(llama_context * ctx, llama_sampler * smpl, i
|
||||
n_past++;
|
||||
}
|
||||
|
||||
llama_synchronize(ctx);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -55,10 +55,10 @@
|
||||
| `-dt, --defrag-thold N` | KV cache defragmentation threshold (DEPRECATED)<br/>(env: LLAMA_ARG_DEFRAG_THOLD) |
|
||||
| `-np, --parallel N` | number of parallel sequences to decode (default: 1)<br/>(env: LLAMA_ARG_N_PARALLEL) |
|
||||
| `--rpc SERVERS` | comma-separated list of RPC servers (host:port)<br/>(env: LLAMA_ARG_RPC) |
|
||||
| `--mlock` | DEPRECATED in favor of `--load-mode`: mmap + force system to keep model in RAM rather than swapping or compressing<br/>(env: LLAMA_ARG_MLOCK) |
|
||||
| `--mlock` | DEPRECATED in favor of `--load-mode`: force system to keep model in RAM rather than swapping or compressing<br/>(env: LLAMA_ARG_MLOCK) |
|
||||
| `--mmap, --no-mmap` | DEPRECATED in favor of `--load-mode`: whether to memory-map model. (if mmap disabled, slower load but may reduce pageouts if not using mlock)<br/>(env: LLAMA_ARG_MMAP) |
|
||||
| `-dio, --direct-io, -ndio, --no-direct-io` | DEPRECATED in favor of `--load-mode`: use DirectIO if available<br/>(env: LLAMA_ARG_DIO) |
|
||||
| `-lm, --load-mode MODE` | model loading mode (default: mmap)<br/>- none: no special loading mode<br/>- mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)<br/>- mlock: mmap + force system to keep model in RAM rather than swapping or compressing<br/>- dio: use DirectIO if available<br/><br/>(env: LLAMA_ARG_LOAD_MODE) |
|
||||
| `-lm, --load-mode MODE` | model loading mode (default: mmap)<br/>- none: no special loading mode<br/>- mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)<br/>- mlock: force system to keep model in RAM rather than swapping or compressing<br/>- mmap+mlock: mmap + force system to keep model in RAM rather than swapping or compressing<br/>- dio: use DirectIO if available<br/><br/>(env: LLAMA_ARG_LOAD_MODE) |
|
||||
| `--numa TYPE` | attempt optimizations that help on some NUMA systems<br/>- distribute: spread execution evenly over all nodes<br/>- isolate: only spawn threads on CPUs on the node that execution started on<br/>- numactl: use the CPU map provided by numactl<br/>if run without this previously, it is recommended to drop the system page cache before using this<br/>see https://github.com/ggml-org/llama.cpp/issues/1437<br/>(env: LLAMA_ARG_NUMA) |
|
||||
| `-dev, --device <dev1,dev2,..>` | comma-separated list of devices to use for offloading (none = don't offload)<br/>use --list-devices to see a list of available devices<br/>(env: LLAMA_ARG_DEVICE) |
|
||||
| `--list-devices` | print list of available devices and exit |
|
||||
|
||||
@@ -138,10 +138,10 @@ llama-completion.exe -m models\gemma-1.1-7b-it.Q4_K_M.gguf --ignore-eos -n -1
|
||||
| `-dt, --defrag-thold N` | KV cache defragmentation threshold (DEPRECATED)<br/>(env: LLAMA_ARG_DEFRAG_THOLD) |
|
||||
| `-np, --parallel N` | number of parallel sequences to decode (default: 1)<br/>(env: LLAMA_ARG_N_PARALLEL) |
|
||||
| `--rpc SERVERS` | comma-separated list of RPC servers (host:port)<br/>(env: LLAMA_ARG_RPC) |
|
||||
| `--mlock` | DEPRECATED in favor of `--load-mode`: mmap + force system to keep model in RAM rather than swapping or compressing<br/>(env: LLAMA_ARG_MLOCK) |
|
||||
| `--mlock` | DEPRECATED in favor of `--load-mode`: force system to keep model in RAM rather than swapping or compressing<br/>(env: LLAMA_ARG_MLOCK) |
|
||||
| `--mmap, --no-mmap` | DEPRECATED in favor of `--load-mode`: whether to memory-map model. (if mmap disabled, slower load but may reduce pageouts if not using mlock)<br/>(env: LLAMA_ARG_MMAP) |
|
||||
| `-dio, --direct-io, -ndio, --no-direct-io` | DEPRECATED in favor of `--load-mode`: use DirectIO if available<br/>(env: LLAMA_ARG_DIO) |
|
||||
| `-lm, --load-mode MODE` | model loading mode (default: mmap)<br/>- none: no special loading mode<br/>- mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)<br/>- mlock: mmap + force system to keep model in RAM rather than swapping or compressing<br/>- dio: use DirectIO if available<br/><br/>(env: LLAMA_ARG_LOAD_MODE) |
|
||||
| `-lm, --load-mode MODE` | model loading mode (default: mmap)<br/>- none: no special loading mode<br/>- mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)<br/>- mlock: force system to keep model in RAM rather than swapping or compressing<br/>- mmap+mlock: mmap + force system to keep model in RAM rather than swapping or compressing<br/>- dio: use DirectIO if available<br/><br/>(env: LLAMA_ARG_LOAD_MODE) |
|
||||
| `--numa TYPE` | attempt optimizations that help on some NUMA systems<br/>- distribute: spread execution evenly over all nodes<br/>- isolate: only spawn threads on CPUs on the node that execution started on<br/>- numactl: use the CPU map provided by numactl<br/>if run without this previously, it is recommended to drop the system page cache before using this<br/>see https://github.com/ggml-org/llama.cpp/issues/1437<br/>(env: LLAMA_ARG_NUMA) |
|
||||
| `-dev, --device <dev1,dev2,..>` | comma-separated list of devices to use for offloading (none = don't offload)<br/>use --list-devices to see a list of available devices<br/>(env: LLAMA_ARG_DEVICE) |
|
||||
| `--list-devices` | print list of available devices and exit |
|
||||
|
||||
@@ -429,45 +429,45 @@ static void print_usage(int /* argc */, char ** argv) {
|
||||
}
|
||||
printf("\n");
|
||||
printf("test parameters:\n");
|
||||
printf(" -m, --model <filename> (default: %s)\n", join(cmd_params_defaults.model, ",").c_str());
|
||||
printf(" -hf, -hfr, --hf-repo <user>/<model>[:quant] Hugging Face model repository; quant is optional, case-insensitive\n");
|
||||
printf(" default to Q4_K_M, or falls back to the first file in the repo if Q4_K_M doesn't exist.\n");
|
||||
printf(" example: ggml-org/GLM-4.7-Flash-GGUF:Q4_K_M\n");
|
||||
printf(" (default: unused)\n");
|
||||
printf(" -hff, --hf-file <file> Hugging Face model file. If specified, it will override the quant in --hf-repo\n");
|
||||
printf(" (default: unused)\n");
|
||||
printf(" -hft, --hf-token <token> Hugging Face access token\n");
|
||||
printf(" (default: value from HF_TOKEN environment variable)\n");
|
||||
printf(" --offline Offline mode: forces use of cache, prevents network access\n");
|
||||
printf(" (default: disabled)\n");
|
||||
printf(" -p, --n-prompt <n> (default: %s)\n", join(cmd_params_defaults.n_prompt, ",").c_str());
|
||||
printf(" -n, --n-gen <n> (default: %s)\n", join(cmd_params_defaults.n_gen, ",").c_str());
|
||||
printf(" -pg <pp,tg> (default: %s)\n", join(transform_to_str(cmd_params_defaults.n_pg, pair_str), ",").c_str());
|
||||
printf(" -d, --n-depth <n> (default: %s)\n", join(cmd_params_defaults.n_depth, ",").c_str());
|
||||
printf(" -b, --batch-size <n> (default: %s)\n", join(cmd_params_defaults.n_batch, ",").c_str());
|
||||
printf(" -ub, --ubatch-size <n> (default: %s)\n", join(cmd_params_defaults.n_ubatch, ",").c_str());
|
||||
printf(" -ctk, --cache-type-k <t> (default: %s)\n", join(transform_to_str(cmd_params_defaults.type_k, ggml_type_name), ",").c_str());
|
||||
printf(" -ctv, --cache-type-v <t> (default: %s)\n", join(transform_to_str(cmd_params_defaults.type_v, ggml_type_name), ",").c_str());
|
||||
printf(" -t, --threads <n> (default: %s)\n", join(cmd_params_defaults.n_threads, ",").c_str());
|
||||
printf(" -C, --cpu-mask <hex,hex> (default: %s)\n", join(cmd_params_defaults.cpu_mask, ",").c_str());
|
||||
printf(" --cpu-strict <0|1> (default: %s)\n", join(cmd_params_defaults.cpu_strict, ",").c_str());
|
||||
printf(" --poll <0...100> (default: %s)\n", join(cmd_params_defaults.poll, ",").c_str());
|
||||
printf(" -ngl, --n-gpu-layers <n> (default: %s)\n", join(cmd_params_defaults.n_gpu_layers, ",").c_str());
|
||||
printf(" -ncmoe, --n-cpu-moe <n> (default: %s)\n", join(cmd_params_defaults.n_cpu_moe, ",").c_str());
|
||||
printf(" -sm, --split-mode <none|layer|row|tensor> (default: %s)\n", join(transform_to_str(cmd_params_defaults.split_mode, split_mode_str), ",").c_str());
|
||||
printf(" -mg, --main-gpu <i> (default: %s)\n", join(cmd_params_defaults.main_gpu, ",").c_str());
|
||||
printf(" -nkvo, --no-kv-offload <0|1> (default: %s)\n", join(cmd_params_defaults.no_kv_offload, ",").c_str());
|
||||
printf(" -fa, --flash-attn <on|off|auto> (default: %s)\n", join(transform_to_str(cmd_params_defaults.flash_attn, llama_flash_attn_type_name), ",").c_str());
|
||||
printf(" -dev, --device <dev0/dev1/...> (default: auto)\n");
|
||||
printf(" -lm, --load-mode <none|mmap|mlock|dio> (default: %s)\n", join(transform_to_str(cmd_params_defaults.load_mode, llama_load_mode_name), ",").c_str());
|
||||
printf(" -mmp, --mmap <0|1> (DEPRECATED IN FAVOUR OF --load-mode)\n");
|
||||
printf(" -dio, --direct-io <0|1> (DEPRECATED IN FAVOUR OF --load-mode)\n");
|
||||
printf(" -embd, --embeddings <0|1> (default: %s)\n", join(cmd_params_defaults.embeddings, ",").c_str());
|
||||
printf(" -ts, --tensor-split <ts0/ts1/..> (default: 0)\n");
|
||||
printf(" -m, --model <filename> (default: %s)\n", join(cmd_params_defaults.model, ",").c_str());
|
||||
printf(" -hf, -hfr, --hf-repo <user>/<model>[:quant] Hugging Face model repository; quant is optional, case-insensitive\n");
|
||||
printf(" default to Q4_K_M, or falls back to the first file in the repo if Q4_K_M doesn't exist.\n");
|
||||
printf(" example: ggml-org/GLM-4.7-Flash-GGUF:Q4_K_M\n");
|
||||
printf(" (default: unused)\n");
|
||||
printf(" -hff, --hf-file <file> Hugging Face model file. If specified, it will override the quant in --hf-repo\n");
|
||||
printf(" (default: unused)\n");
|
||||
printf(" -hft, --hf-token <token> Hugging Face access token\n");
|
||||
printf(" (default: value from HF_TOKEN environment variable)\n");
|
||||
printf(" --offline Offline mode: forces use of cache, prevents network access\n");
|
||||
printf(" (default: disabled)\n");
|
||||
printf(" -p, --n-prompt <n> (default: %s)\n", join(cmd_params_defaults.n_prompt, ",").c_str());
|
||||
printf(" -n, --n-gen <n> (default: %s)\n", join(cmd_params_defaults.n_gen, ",").c_str());
|
||||
printf(" -pg <pp,tg> (default: %s)\n", join(transform_to_str(cmd_params_defaults.n_pg, pair_str), ",").c_str());
|
||||
printf(" -d, --n-depth <n> (default: %s)\n", join(cmd_params_defaults.n_depth, ",").c_str());
|
||||
printf(" -b, --batch-size <n> (default: %s)\n", join(cmd_params_defaults.n_batch, ",").c_str());
|
||||
printf(" -ub, --ubatch-size <n> (default: %s)\n", join(cmd_params_defaults.n_ubatch, ",").c_str());
|
||||
printf(" -ctk, --cache-type-k <t> (default: %s)\n", join(transform_to_str(cmd_params_defaults.type_k, ggml_type_name), ",").c_str());
|
||||
printf(" -ctv, --cache-type-v <t> (default: %s)\n", join(transform_to_str(cmd_params_defaults.type_v, ggml_type_name), ",").c_str());
|
||||
printf(" -t, --threads <n> (default: %s)\n", join(cmd_params_defaults.n_threads, ",").c_str());
|
||||
printf(" -C, --cpu-mask <hex,hex> (default: %s)\n", join(cmd_params_defaults.cpu_mask, ",").c_str());
|
||||
printf(" --cpu-strict <0|1> (default: %s)\n", join(cmd_params_defaults.cpu_strict, ",").c_str());
|
||||
printf(" --poll <0...100> (default: %s)\n", join(cmd_params_defaults.poll, ",").c_str());
|
||||
printf(" -ngl, --n-gpu-layers <n> (default: %s)\n", join(cmd_params_defaults.n_gpu_layers, ",").c_str());
|
||||
printf(" -ncmoe, --n-cpu-moe <n> (default: %s)\n", join(cmd_params_defaults.n_cpu_moe, ",").c_str());
|
||||
printf(" -sm, --split-mode <none|layer|row|tensor> (default: %s)\n", join(transform_to_str(cmd_params_defaults.split_mode, split_mode_str), ",").c_str());
|
||||
printf(" -mg, --main-gpu <i> (default: %s)\n", join(cmd_params_defaults.main_gpu, ",").c_str());
|
||||
printf(" -nkvo, --no-kv-offload <0|1> (default: %s)\n", join(cmd_params_defaults.no_kv_offload, ",").c_str());
|
||||
printf(" -fa, --flash-attn <on|off|auto> (default: %s)\n", join(transform_to_str(cmd_params_defaults.flash_attn, llama_flash_attn_type_name), ",").c_str());
|
||||
printf(" -dev, --device <dev0/dev1/...> (default: auto)\n");
|
||||
printf(" -lm, --load-mode <none|mmap|mlock|mmap+mlock|dio> (default: %s)\n", join(transform_to_str(cmd_params_defaults.load_mode, llama_load_mode_name), ",").c_str());
|
||||
printf(" -mmp, --mmap <0|1> (DEPRECATED IN FAVOUR OF --load-mode)\n");
|
||||
printf(" -dio, --direct-io <0|1> (DEPRECATED IN FAVOUR OF --load-mode)\n");
|
||||
printf(" -embd, --embeddings <0|1> (default: %s)\n", join(cmd_params_defaults.embeddings, ",").c_str());
|
||||
printf(" -ts, --tensor-split <ts0/ts1/..> (default: 0)\n");
|
||||
printf(" -ot --override-tensor <tensor name pattern>=<buffer type>;...\n");
|
||||
printf(" (default: disabled)\n");
|
||||
printf(" -nopo, --no-op-offload <0|1> (default: 0)\n");
|
||||
printf(" --no-host <0|1> (default: %s)\n", join(cmd_params_defaults.no_host, ",").c_str());
|
||||
printf(" (default: disabled)\n");
|
||||
printf(" -nopo, --no-op-offload <0|1> (default: 0)\n");
|
||||
printf(" --no-host <0|1> (default: %s)\n", join(cmd_params_defaults.no_host, ",").c_str());
|
||||
printf("\n");
|
||||
printf(
|
||||
"Multiple values can be given for each parameter by separating them with ','\n"
|
||||
@@ -670,22 +670,7 @@ static cmd_params parse_cmd_params(int argc, char ** argv) {
|
||||
break;
|
||||
}
|
||||
} else if (arg == "--list-devices") {
|
||||
std::vector<ggml_backend_dev_t> devices;
|
||||
for (size_t i = 0; i < ggml_backend_dev_count(); ++i) {
|
||||
auto * dev = ggml_backend_dev_get(i);
|
||||
if (ggml_backend_dev_type(dev) != GGML_BACKEND_DEVICE_TYPE_CPU) {
|
||||
devices.push_back(dev);
|
||||
}
|
||||
}
|
||||
printf("Available devices:\n");
|
||||
if (devices.empty()) {
|
||||
printf(" (none)\n");
|
||||
}
|
||||
for (auto * dev : devices) {
|
||||
size_t free, total;
|
||||
ggml_backend_dev_memory(dev, &free, &total);
|
||||
printf(" %s: %s (%zu MiB, %zu MiB free)\n", ggml_backend_dev_name(dev), ggml_backend_dev_description(dev), total / 1024 / 1024, free / 1024 / 1024);
|
||||
}
|
||||
common_print_available_devices();
|
||||
exit(0);
|
||||
} else if (arg == "-t" || arg == "--threads") {
|
||||
if (++i >= argc) {
|
||||
@@ -785,6 +770,8 @@ static cmd_params parse_cmd_params(int argc, char ** argv) {
|
||||
mode = LLAMA_LOAD_MODE_MMAP;
|
||||
} else if (m == "mlock") {
|
||||
mode = LLAMA_LOAD_MODE_MLOCK;
|
||||
} else if (m == "mmap+mlock") {
|
||||
mode = LLAMA_LOAD_MODE_MMAP_MLOCK;
|
||||
} else if (m == "dio") {
|
||||
mode = LLAMA_LOAD_MODE_DIRECT_IO;
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
# mtmd
|
||||
|
||||
set(MTMD_VIDEO ON CACHE BOOL "enable video support in mtmd (requires ffmpeg binary in PATH)")
|
||||
set(MTMD_VIDEO_HELP "enable video support in mtmd (requires ffmpeg binary in PATH)")
|
||||
|
||||
set(MTMD_VIDEO ON CACHE BOOL "${MTMD_VIDEO_HELP}")
|
||||
# TODO: add MTMD_VIDEO_METHOD in the future to select between ffmpeg and other backends
|
||||
|
||||
if (MTMD_VIDEO AND NOT LLAMA_SUBPROCESS)
|
||||
message(STATUS "Disabling MTMD_VIDEO because LLAMA_SUBPROCESS is OFF")
|
||||
set(MTMD_VIDEO OFF CACHE BOOL "${MTMD_VIDEO_HELP}" FORCE)
|
||||
endif()
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
add_library(mtmd
|
||||
@@ -40,9 +47,11 @@ add_library(mtmd
|
||||
models/paddleocr.cpp
|
||||
models/pixtral.cpp
|
||||
models/qwen2vl.cpp
|
||||
models/minimax-m3.cpp
|
||||
models/qwen3vl.cpp
|
||||
models/mimovl.cpp
|
||||
models/qwen3a.cpp
|
||||
models/mimo-audio.cpp
|
||||
models/step3vl.cpp
|
||||
models/siglip.cpp
|
||||
models/whisper-enc.cpp
|
||||
|
||||
@@ -13,6 +13,14 @@
|
||||
|
||||
struct build_vit_opts {
|
||||
ggml_tensor * attn_mask = nullptr;
|
||||
// TODO @ngxson : merge attn_mask and attn_mask_layers into one call
|
||||
std::vector<ggml_tensor *> attn_mask_layers; // one per layer
|
||||
|
||||
// hook at layer output embeddings
|
||||
std::function<void(ggml_tensor * cur, int il)> callback_layer_out = nullptr;
|
||||
|
||||
// whether to skip the automatic post-layernorm (model.post_ln_w) applied at the end
|
||||
bool skip_post_ln = false;
|
||||
};
|
||||
|
||||
struct clip_graph {
|
||||
|
||||
@@ -82,6 +82,12 @@
|
||||
#define KEY_A_PROJ_WINDOW_SIZE "clip.audio.projector.window_size"
|
||||
#define KEY_A_PROJ_DOWNSAMPLE_RATE "clip.audio.projector.downsample_rate"
|
||||
#define KEY_A_PROJ_HEAD_COUNT "clip.audio.projector.head_count"
|
||||
#define KEY_A_RVQ_NUM_QUANTIZERS "clip.audio.rvq.num_quantizers" // mimo-audio-tokenizer
|
||||
#define KEY_A_RVQ_CODEBOOK_SIZE "clip.audio.rvq.codebook_size" // mimo-audio-tokenizer: per-quantizer bin count
|
||||
#define KEY_A_WA_PATTERN_MODE "clip.audio.wa_pattern_mode" // mimo-audio-tokenizer, per-layer -1 (full) / 0 (windowed)
|
||||
#define KEY_A_ATTN_WINDOW_SIZE "clip.audio.window_size" // mimo-audio-tokenizer: sliding-window radius
|
||||
#define KEY_A_LOCAL_BLOCK_COUNT "clip.audio.local_block_count" // mimo-v2.5: input_local_transformer layer count
|
||||
#define KEY_A_LOCAL_GROUP_SIZE "clip.audio.local_group_size" // mimo-v2.5: input_local_transformer grouping size
|
||||
|
||||
//
|
||||
// tensor name constants
|
||||
@@ -131,6 +137,8 @@
|
||||
#define TN_MM_SOFT_EMB_N "mm.soft_emb_norm.weight" // gemma3
|
||||
#define TN_MM_PROJECTOR "mm.model.fc.%s" // idefics3, deepseekocr
|
||||
#define TN_MM_PATCH_MERGER "mm.patch_merger.%s" // mistral small 3.1, glm4v
|
||||
#define TN_MM_MERGER_FC1 "mm.merger.fc1.%s" // minimax-m3 patch-merge MLP
|
||||
#define TN_MM_MERGER_FC2 "mm.merger.fc2.%s"
|
||||
#define TN_TOK_IMG_BREAK "v.token_embd.img_break" // pixtral
|
||||
#define TN_TOK_GLM_BOI "adapter.boi" // glm-edge (these embeddings are not in text model)
|
||||
#define TN_TOK_GLM_EOI "adapter.eoi" // glm-edge (these embeddings are not in text model)
|
||||
@@ -173,6 +181,24 @@
|
||||
#define TN_MM_NORM_PRE "mm.a.norm_pre.%s"
|
||||
#define TN_MM_NORM_MID "mm.a.norm_mid.%s"
|
||||
|
||||
// mimo-audio-tokenizer
|
||||
#define TN_A_DOWNSAMPLE_CONV "a.downsample.conv.%s"
|
||||
#define TN_A_DOWNSAMPLE_NORM "a.downsample.norm.%s"
|
||||
#define TN_A_RVQ_CODEBOOK "a.rvq.codebook.%s"
|
||||
// mimo-v2.5: text-side RVQ code embedding ("text codebook")
|
||||
#define TN_MM_A_CODE_EMBD "mm.a.code_embd.%s"
|
||||
// mimo-v2.5: LLM-side connector (input_local_transformer)
|
||||
#define TN_MM_A_LOCAL_ATTN_Q "mm.a.local_blk.%d.attn_q.%s"
|
||||
#define TN_MM_A_LOCAL_ATTN_K "mm.a.local_blk.%d.attn_k.%s"
|
||||
#define TN_MM_A_LOCAL_ATTN_V "mm.a.local_blk.%d.attn_v.%s"
|
||||
#define TN_MM_A_LOCAL_ATTN_OUT "mm.a.local_blk.%d.attn_out.%s"
|
||||
#define TN_MM_A_LOCAL_FFN_GATE "mm.a.local_blk.%d.ffn_gate.%s"
|
||||
#define TN_MM_A_LOCAL_FFN_UP "mm.a.local_blk.%d.ffn_up.%s"
|
||||
#define TN_MM_A_LOCAL_FFN_DOWN "mm.a.local_blk.%d.ffn_down.%s"
|
||||
#define TN_MM_A_LOCAL_LN1 "mm.a.local_blk.%d.ln1.%s"
|
||||
#define TN_MM_A_LOCAL_LN2 "mm.a.local_blk.%d.ln2.%s"
|
||||
#define TN_MM_A_LOCAL_NORM "mm.a.local_norm.%s"
|
||||
|
||||
// cogvlm
|
||||
#define TN_MM_POST_FC_NORM "mm.post_fc_norm.%s"
|
||||
#define TN_MM_H_TO_4H "mm.up.%s"
|
||||
@@ -370,7 +396,9 @@ enum projector_type {
|
||||
PROJECTOR_TYPE_MINICPMV4_6,
|
||||
PROJECTOR_TYPE_GRANITE_SPEECH,
|
||||
PROJECTOR_TYPE_MIMOVL,
|
||||
PROJECTOR_TYPE_MINIMAX_M3,
|
||||
PROJECTOR_TYPE_GRANITE4_VISION,
|
||||
PROJECTOR_TYPE_MIMO_AUDIO,
|
||||
PROJECTOR_TYPE_UNKNOWN,
|
||||
};
|
||||
|
||||
@@ -424,7 +452,9 @@ static std::map<projector_type, std::string> PROJECTOR_TYPE_NAMES = {
|
||||
{ PROJECTOR_TYPE_MINICPMV4_6, "minicpmv4_6"},
|
||||
{ PROJECTOR_TYPE_GRANITE_SPEECH, "granite_speech"},
|
||||
{ PROJECTOR_TYPE_MIMOVL, "mimovl"},
|
||||
{ PROJECTOR_TYPE_MINIMAX_M3, "minimax_m3"},
|
||||
{ PROJECTOR_TYPE_GRANITE4_VISION, "granite4_vision"},
|
||||
{ PROJECTOR_TYPE_MIMO_AUDIO, "mimo_audio"},
|
||||
};
|
||||
|
||||
static projector_type clip_projector_type_from_string(const std::string & str) {
|
||||
|
||||
@@ -124,6 +124,14 @@ struct clip_hparams {
|
||||
int32_t audio_window_len = -1;
|
||||
int32_t audio_hop_len = -1;
|
||||
|
||||
// mimo-audio-tokenizer: residual vector quantizer
|
||||
int32_t rvq_num_quantizers = 0;
|
||||
std::vector<int32_t> rvq_codebook_size; // per-quantizer bin count (ragged, e.g. 1024/1024/256/128x17)
|
||||
|
||||
// mimo-v2.5: LLM-side connector (input_local_transformer)
|
||||
int32_t audio_local_n_layer = 0;
|
||||
int32_t audio_local_group_size = 0;
|
||||
|
||||
// legacy
|
||||
bool has_llava_projector = false;
|
||||
int minicpmv_version = 0;
|
||||
@@ -397,6 +405,10 @@ struct clip_model {
|
||||
ggml_tensor * mm_0_b = nullptr;
|
||||
ggml_tensor * mm_2_w = nullptr;
|
||||
ggml_tensor * mm_2_b = nullptr;
|
||||
ggml_tensor * mm_merger_fc1_w = nullptr; // minimax-m3
|
||||
ggml_tensor * mm_merger_fc1_b = nullptr;
|
||||
ggml_tensor * mm_merger_fc2_w = nullptr;
|
||||
ggml_tensor * mm_merger_fc2_b = nullptr;
|
||||
|
||||
ggml_tensor * image_newline = nullptr;
|
||||
ggml_tensor * view_seperator = nullptr;
|
||||
@@ -533,6 +545,20 @@ struct clip_model {
|
||||
ggml_tensor * mm_norm_pre_b = nullptr;
|
||||
ggml_tensor * mm_norm_mid_w = nullptr;
|
||||
|
||||
// mimo-audio-tokenizer: post-transformer downsample + RVQ codebook
|
||||
ggml_tensor * downsample_conv_w = nullptr; // no bias
|
||||
ggml_tensor * downsample_norm_w = nullptr;
|
||||
ggml_tensor * downsample_norm_b = nullptr;
|
||||
ggml_tensor * rvq_codebook = nullptr; // merged 3D [n_q, max_bins, dim]
|
||||
|
||||
// mimo-v2.5: text-side RVQ code embedding ("text codebook")
|
||||
ggml_tensor * mm_a_code_embd = nullptr; // merged 3D [n_channels, vocab, dim]
|
||||
|
||||
// mimo-v2.5: LLM-side connector (input_local_transformer, separate from the
|
||||
// audio_tokenizer's own encoder `layers`)
|
||||
std::vector<clip_layer> mm_a_local_layers;
|
||||
ggml_tensor * mm_a_local_norm_w = nullptr;
|
||||
|
||||
// qwen3a
|
||||
ggml_tensor * conv2d_1_w = nullptr;
|
||||
ggml_tensor * conv2d_1_b = nullptr;
|
||||
|
||||
+214
-2
@@ -340,6 +340,11 @@ ggml_tensor * clip_graph::build_vit(
|
||||
auto & layer = model.layers[il];
|
||||
ggml_tensor * cur = inpL; // inpL = residual, cur = hidden_states
|
||||
|
||||
ggml_tensor * attn_mask = opts.attn_mask;
|
||||
if (opts.attn_mask_layers.size() > (size_t) il) {
|
||||
attn_mask = opts.attn_mask_layers[il];
|
||||
}
|
||||
|
||||
// layernorm1
|
||||
cur = build_norm(cur, layer.ln_1_w, layer.ln_1_b, norm_t, eps, il);
|
||||
cb(cur, "layer_inp_normed", il);
|
||||
@@ -452,7 +457,7 @@ ggml_tensor * clip_graph::build_vit(
|
||||
|
||||
// build_attn returns a flat 2D [n_embd, n_pos*B]
|
||||
cur = build_attn(layer.o_w, layer.o_b,
|
||||
Qcur, Kcur, Vcur, opts.attn_mask, kq_scale, il);
|
||||
Qcur, Kcur, Vcur, attn_mask, kq_scale, il);
|
||||
cb(cur, "attn_out", il);
|
||||
}
|
||||
|
||||
@@ -471,6 +476,10 @@ ggml_tensor * clip_graph::build_vit(
|
||||
|
||||
inpL = cur; // inpL = residual, cur = hidden_states
|
||||
|
||||
if (opts.callback_layer_out) {
|
||||
opts.callback_layer_out(cur, il);
|
||||
}
|
||||
|
||||
cb(cur, "ffn_inp", il);
|
||||
|
||||
// layernorm2 (pre-ffn norm)
|
||||
@@ -519,7 +528,7 @@ ggml_tensor * clip_graph::build_vit(
|
||||
}
|
||||
|
||||
// post-layernorm
|
||||
if (model.post_ln_w) {
|
||||
if (model.post_ln_w && !opts.skip_post_ln) {
|
||||
inpL = build_norm(inpL, model.post_ln_w, model.post_ln_b, norm_t, eps, -1);
|
||||
}
|
||||
|
||||
@@ -915,6 +924,10 @@ static std::unique_ptr<clip_graph> clip_get_graph_builder(clip_ctx * ctx, const
|
||||
{
|
||||
builder = std::make_unique<clip_graph_mimovl>(ctx, img);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_MINIMAX_M3:
|
||||
{
|
||||
builder = std::make_unique<clip_graph_minimax_m3>(ctx, img);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_STEP3VL:
|
||||
{
|
||||
builder = std::make_unique<clip_graph_step3vl>(ctx, img);
|
||||
@@ -1008,6 +1021,10 @@ static std::unique_ptr<clip_graph> clip_get_graph_builder(clip_ctx * ctx, const
|
||||
{
|
||||
builder = std::make_unique<clip_graph_qwen3a>(ctx, img);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_MIMO_AUDIO:
|
||||
{
|
||||
builder = std::make_unique<clip_graph_mimo_audio>(ctx, img);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_YOUTUVL:
|
||||
{
|
||||
builder = std::make_unique<clip_graph_youtuvl>(ctx, img);
|
||||
@@ -1469,6 +1486,17 @@ struct clip_model_loader {
|
||||
LOG_WRN("%s: more info: https://github.com/ggml-org/llama.cpp/issues/16842\n\n", __func__);
|
||||
}
|
||||
} break;
|
||||
case PROJECTOR_TYPE_MINIMAX_M3:
|
||||
{
|
||||
hparams.n_merge = 2; // spatial_merge_size
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC_PILLOW;
|
||||
hparams.image_resize_pad = PAD_NONE;
|
||||
get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge, false);
|
||||
hparams.rope_theta = 10000.0f; // vision_config.rope_theta
|
||||
// MiniMax-M3: max_pixels 451584 (=672^2) -> 576 merged tokens (image_seq_length)
|
||||
hparams.set_limit_image_tokens(8, 576);
|
||||
hparams.set_warmup_n_tokens(16*16);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_MIMOVL:
|
||||
{
|
||||
hparams.n_merge = 2; // spatial_merge_size
|
||||
@@ -1560,6 +1588,45 @@ struct clip_model_loader {
|
||||
hparams.audio_window_len = 400;
|
||||
hparams.audio_hop_len = 160;
|
||||
} break;
|
||||
case PROJECTOR_TYPE_MIMO_AUDIO:
|
||||
{
|
||||
get_u32(KEY_A_RVQ_NUM_QUANTIZERS, hparams.rvq_num_quantizers, false);
|
||||
get_arr_int(KEY_A_RVQ_CODEBOOK_SIZE, hparams.rvq_codebook_size, false);
|
||||
if (hparams.rvq_num_quantizers <= 0) {
|
||||
throw std::runtime_error(string_format("%s: mimo_audio: missing %s\n", __func__, KEY_A_RVQ_NUM_QUANTIZERS));
|
||||
}
|
||||
if ((int) hparams.rvq_codebook_size.size() != hparams.rvq_num_quantizers) {
|
||||
throw std::runtime_error(string_format(
|
||||
"%s: mimo_audio: %s length (%zu) must equal %s (%d)\n", __func__,
|
||||
KEY_A_RVQ_CODEBOOK_SIZE, hparams.rvq_codebook_size.size(),
|
||||
KEY_A_RVQ_NUM_QUANTIZERS, hparams.rvq_num_quantizers));
|
||||
}
|
||||
hparams.ffn_op = FFN_GELU_ERF; // PyTorch F.gelu default (approximate="none")
|
||||
hparams.rope_theta = 10000.0f;
|
||||
|
||||
// audio preprocessing params (mel spectrogram)
|
||||
hparams.audio_sample_rate = 24000;
|
||||
hparams.audio_n_fft = 960;
|
||||
hparams.audio_window_len = 960;
|
||||
hparams.audio_hop_len = 240;
|
||||
|
||||
get_u32(KEY_A_ATTN_WINDOW_SIZE, hparams.attn_window_size);
|
||||
std::vector<int> wa_pattern;
|
||||
get_arr_int(KEY_A_WA_PATTERN_MODE, wa_pattern, true);
|
||||
if ((int) wa_pattern.size() != hparams.n_layer) {
|
||||
throw std::runtime_error(string_format(
|
||||
"%s: mimo_audio: %s length (%zu) must equal n_layer (%d)\n", __func__,
|
||||
KEY_A_WA_PATTERN_MODE, wa_pattern.size(), hparams.n_layer));
|
||||
}
|
||||
hparams.wa_pattern_mode.assign(wa_pattern.begin(), wa_pattern.end());
|
||||
|
||||
get_u32(KEY_A_LOCAL_BLOCK_COUNT, hparams.audio_local_n_layer);
|
||||
get_u32(KEY_A_LOCAL_GROUP_SIZE, hparams.audio_local_group_size);
|
||||
if (hparams.audio_local_group_size <= 0) {
|
||||
throw std::runtime_error(string_format(
|
||||
"%s: mimo_audio: %s must be > 0\n", __func__, KEY_A_LOCAL_GROUP_SIZE));
|
||||
}
|
||||
} break;
|
||||
case PROJECTOR_TYPE_PADDLEOCR:
|
||||
{
|
||||
hparams.n_merge = 2;
|
||||
@@ -2089,6 +2156,19 @@ struct clip_model_loader {
|
||||
model.mm_1_w = get_tensor(string_format(TN_LLAVA_PROJ, 2, "weight"));
|
||||
model.mm_1_b = get_tensor(string_format(TN_LLAVA_PROJ, 2, "bias"), false);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_MINIMAX_M3:
|
||||
{
|
||||
// per-patch MLP: mm.1 -> gelu -> mm.2
|
||||
model.mm_1_w = get_tensor(string_format(TN_LLAVA_PROJ, 1, "weight"));
|
||||
model.mm_1_b = get_tensor(string_format(TN_LLAVA_PROJ, 1, "bias"));
|
||||
model.mm_2_w = get_tensor(string_format(TN_LLAVA_PROJ, 2, "weight"));
|
||||
model.mm_2_b = get_tensor(string_format(TN_LLAVA_PROJ, 2, "bias"));
|
||||
// 2x2 merge MLP: mm.merge.fc1 -> gelu -> mm.merge.fc2
|
||||
model.mm_merger_fc1_w = get_tensor(string_format(TN_MM_MERGER_FC1, "weight"));
|
||||
model.mm_merger_fc1_b = get_tensor(string_format(TN_MM_MERGER_FC1, "bias"));
|
||||
model.mm_merger_fc2_w = get_tensor(string_format(TN_MM_MERGER_FC2, "weight"));
|
||||
model.mm_merger_fc2_b = get_tensor(string_format(TN_MM_MERGER_FC2, "bias"));
|
||||
} break;
|
||||
case PROJECTOR_TYPE_STEP3VL:
|
||||
{
|
||||
model.mm_0_w = get_tensor(string_format(TN_LLAVA_PROJ, 0, "weight"));
|
||||
@@ -2416,6 +2496,54 @@ struct clip_model_loader {
|
||||
model.mm_2_w = get_tensor(string_format(TN_MM_AUDIO_MLP, 2, "weight"));
|
||||
model.mm_2_b = get_tensor(string_format(TN_MM_AUDIO_MLP, 2, "bias"));
|
||||
} break;
|
||||
case PROJECTOR_TYPE_MIMO_AUDIO:
|
||||
{
|
||||
model.conv1d_1_w = get_tensor(string_format(TN_CONV1D, 1, "weight"));
|
||||
model.conv1d_1_b = get_tensor(string_format(TN_CONV1D, 1, "bias"));
|
||||
model.conv1d_2_w = get_tensor(string_format(TN_CONV1D, 2, "weight"));
|
||||
model.conv1d_2_b = get_tensor(string_format(TN_CONV1D, 2, "bias"));
|
||||
model.downsample_conv_w = get_tensor(string_format(TN_A_DOWNSAMPLE_CONV, "weight"));
|
||||
model.downsample_norm_w = get_tensor(string_format(TN_A_DOWNSAMPLE_NORM, "weight"));
|
||||
model.downsample_norm_b = get_tensor(string_format(TN_A_DOWNSAMPLE_NORM, "bias"));
|
||||
model.rvq_codebook = get_tensor(string_format(TN_A_RVQ_CODEBOOK, "weight"), false);
|
||||
model.mm_a_code_embd = get_tensor(string_format(TN_MM_A_CODE_EMBD, "weight"), false);
|
||||
if (!model.rvq_codebook || !model.mm_a_code_embd) {
|
||||
throw std::runtime_error(string_format("%s: mimo_audio: missing %s or %s\n", __func__,
|
||||
TN_A_RVQ_CODEBOOK, TN_MM_A_CODE_EMBD));
|
||||
}
|
||||
// hparams.rvq_codebook_size comes from GGUF metadata and is independent of the
|
||||
// tensors' actual shapes - bound it so codebook/code_embd views built from it
|
||||
// (mimo-audio.cpp) can never read past either tensor's allocated bins/vocab.
|
||||
for (int32_t bins : hparams.rvq_codebook_size) {
|
||||
if (bins <= 0 || bins > model.rvq_codebook->ne[1] || bins > model.mm_a_code_embd->ne[1]) {
|
||||
throw std::runtime_error(string_format(
|
||||
"%s: mimo_audio: %s entry (%d) out of range for codebook/code_embd tensors\n",
|
||||
__func__, KEY_A_RVQ_CODEBOOK_SIZE, bins));
|
||||
}
|
||||
}
|
||||
|
||||
// LLM-side connector: input_local_transformer + projection
|
||||
model.mm_a_local_layers.resize(hparams.audio_local_n_layer);
|
||||
for (int il = 0; il < hparams.audio_local_n_layer; il++) {
|
||||
auto & layer = model.mm_a_local_layers[il];
|
||||
layer.q_w = get_tensor(string_format(TN_MM_A_LOCAL_ATTN_Q, il, "weight"));
|
||||
layer.q_b = get_tensor(string_format(TN_MM_A_LOCAL_ATTN_Q, il, "bias"));
|
||||
layer.k_w = get_tensor(string_format(TN_MM_A_LOCAL_ATTN_K, il, "weight"));
|
||||
layer.k_b = get_tensor(string_format(TN_MM_A_LOCAL_ATTN_K, il, "bias"));
|
||||
layer.v_w = get_tensor(string_format(TN_MM_A_LOCAL_ATTN_V, il, "weight"));
|
||||
layer.v_b = get_tensor(string_format(TN_MM_A_LOCAL_ATTN_V, il, "bias"));
|
||||
layer.o_w = get_tensor(string_format(TN_MM_A_LOCAL_ATTN_OUT, il, "weight"));
|
||||
layer.ff_gate_w = get_tensor(string_format(TN_MM_A_LOCAL_FFN_GATE, il, "weight"));
|
||||
layer.ff_up_w = get_tensor(string_format(TN_MM_A_LOCAL_FFN_UP, il, "weight"));
|
||||
layer.ff_down_w = get_tensor(string_format(TN_MM_A_LOCAL_FFN_DOWN, il, "weight"));
|
||||
layer.ln_1_w = get_tensor(string_format(TN_MM_A_LOCAL_LN1, il, "weight"));
|
||||
layer.ln_2_w = get_tensor(string_format(TN_MM_A_LOCAL_LN2, il, "weight"));
|
||||
}
|
||||
model.mm_a_local_norm_w = get_tensor(string_format(TN_MM_A_LOCAL_NORM, "weight"));
|
||||
|
||||
model.mm_1_w = get_tensor(string_format(TN_MM_AUDIO_MLP, 1, "weight"));
|
||||
model.mm_2_w = get_tensor(string_format(TN_MM_AUDIO_MLP, 2, "weight"));
|
||||
} break;
|
||||
case PROJECTOR_TYPE_VOXTRAL:
|
||||
{
|
||||
model.conv1d_1_w = get_tensor(string_format(TN_CONV1D, 1, "weight"));
|
||||
@@ -3360,6 +3488,7 @@ int clip_n_output_tokens(const clip_ctx * ctx, const clip_image_f32 * img) {
|
||||
case PROJECTOR_TYPE_QWEN3VL:
|
||||
case PROJECTOR_TYPE_EXAONE4_5:
|
||||
case PROJECTOR_TYPE_MIMOVL:
|
||||
case PROJECTOR_TYPE_MINIMAX_M3:
|
||||
case PROJECTOR_TYPE_GLM4V:
|
||||
case PROJECTOR_TYPE_YOUTUVL:
|
||||
{
|
||||
@@ -3520,6 +3649,15 @@ int clip_n_output_tokens(const clip_ctx * ctx, const clip_image_f32 * img) {
|
||||
{
|
||||
n_patches = img->nx(); // no downsampling: one token per raw waveform frame
|
||||
} break;
|
||||
case PROJECTOR_TYPE_MIMO_AUDIO:
|
||||
{
|
||||
// conv1(s=1) + conv2(s=2) -> RVQ-encoder downsample conv(k=2,s=2)
|
||||
int n = img->nx();
|
||||
n = (n - 1) / 2 + 1; // conv1 + conv2
|
||||
n = (n - 2) / 2 + 1; // downsample conv
|
||||
const int group_size = params.audio_local_group_size;
|
||||
n_patches = (n + group_size - 1) / group_size;
|
||||
} break;
|
||||
case PROJECTOR_TYPE_GRANITE_SPEECH:
|
||||
{
|
||||
const int ws = ctx->model.hparams.audio_proj_window_size;
|
||||
@@ -3866,6 +4004,24 @@ bool clip_image_batch_encode(clip_ctx * ctx, int n_threads, const clip_image_f32
|
||||
|
||||
set_input_i32("positions", positions);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_MINIMAX_M3:
|
||||
{
|
||||
const int n_merge = hparams.n_merge;
|
||||
const int gh = image_size_height / patch_size;
|
||||
const int gw = image_size_width / patch_size;
|
||||
std::vector<int32_t> pos_h, pos_w;
|
||||
pos_h.reserve(gh * gw);
|
||||
pos_w.reserve(gh * gw);
|
||||
for (int bh = 0; bh < gh / n_merge; bh++)
|
||||
for (int bw = 0; bw < gw / n_merge; bw++)
|
||||
for (int mh = 0; mh < n_merge; mh++)
|
||||
for (int mw = 0; mw < n_merge; mw++) {
|
||||
pos_h.push_back(bh * n_merge + mh);
|
||||
pos_w.push_back(bw * n_merge + mw);
|
||||
}
|
||||
set_input_i32("minimax_pos_h", pos_h);
|
||||
set_input_i32("minimax_pos_w", pos_w);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_DOTS_OCR:
|
||||
{
|
||||
const int pw = image_size_width / patch_size;
|
||||
@@ -4329,6 +4485,58 @@ bool clip_image_batch_encode(clip_ctx * ctx, int n_threads, const clip_image_f32
|
||||
set_input_f32("pos_emb", pos_emb);
|
||||
}
|
||||
} break;
|
||||
case PROJECTOR_TYPE_MIMO_AUDIO:
|
||||
{
|
||||
GGML_ASSERT(imgs.entries.size() == 1);
|
||||
const int n_frames = imgs.entries.front().nx();
|
||||
const int n_pos = (n_frames - 1) / 2 + 1; // matches conv1(s=1)+conv2(s=2) output length
|
||||
|
||||
std::vector<int32_t> positions(n_pos);
|
||||
for (int i = 0; i < n_pos; i++) {
|
||||
positions[i] = i;
|
||||
}
|
||||
set_input_i32("mimo_audio_positions", positions);
|
||||
|
||||
const int window = hparams.attn_window_size;
|
||||
GGML_ASSERT(window > 0);
|
||||
|
||||
const float neg_inf = std::numeric_limits<float>::lowest();
|
||||
std::vector<float> full_mask((size_t) n_pos * n_pos);
|
||||
std::vector<float> window_mask((size_t) n_pos * n_pos);
|
||||
for (int q = 0; q < n_pos; q++) {
|
||||
for (int k = 0; k < n_pos; k++) {
|
||||
const bool causal_ok = k <= q;
|
||||
full_mask[(size_t) q * n_pos + k] = causal_ok ? 0.0f : neg_inf;
|
||||
window_mask[(size_t) q * n_pos + k] = (causal_ok && (q - k) <= window) ? 0.0f : neg_inf;
|
||||
}
|
||||
}
|
||||
set_input_f32("mimo_audio_full_mask", full_mask);
|
||||
set_input_f32("mimo_audio_window_mask", window_mask);
|
||||
|
||||
// input_local_transformer: block-diagonal mask + in-group positions
|
||||
{
|
||||
const int n_pos_ds = (n_pos - 2) / 2 + 1; // matches downsample conv (k=2,s=2,p=0)
|
||||
const int group_size = hparams.audio_local_group_size;
|
||||
GGML_ASSERT(group_size > 0);
|
||||
const int n_groups = (n_pos_ds + group_size - 1) / group_size;
|
||||
const int n_padded = n_groups * group_size;
|
||||
|
||||
std::vector<int32_t> local_positions(n_padded);
|
||||
for (int i = 0; i < n_padded; i++) {
|
||||
local_positions[i] = i % group_size;
|
||||
}
|
||||
set_input_i32("mimo_audio_local_positions", local_positions);
|
||||
|
||||
std::vector<float> local_mask((size_t) n_padded * n_padded);
|
||||
for (int q = 0; q < n_padded; q++) {
|
||||
for (int k = 0; k < n_padded; k++) {
|
||||
const bool same_group = (q / group_size) == (k / group_size);
|
||||
local_mask[(size_t) q * n_padded + k] = same_group ? 0.0f : neg_inf;
|
||||
}
|
||||
}
|
||||
set_input_f32("mimo_audio_local_mask", local_mask);
|
||||
}
|
||||
} break;
|
||||
case PROJECTOR_TYPE_LFM2A:
|
||||
{
|
||||
GGML_ASSERT(imgs.entries.size() == 1);
|
||||
@@ -4569,6 +4777,8 @@ int clip_n_mmproj_embd(const struct clip_ctx * ctx) {
|
||||
return ctx->model.mm_ffn_down_w->ne[1];
|
||||
case PROJECTOR_TYPE_GLM_EDGE:
|
||||
return ctx->model.mm_model_mlp_3_w->ne[1];
|
||||
case PROJECTOR_TYPE_MINIMAX_M3:
|
||||
return ctx->model.mm_merger_fc2_b->ne[0];
|
||||
case PROJECTOR_TYPE_QWEN2VL:
|
||||
case PROJECTOR_TYPE_QWEN25VL:
|
||||
case PROJECTOR_TYPE_EXAONE4_5:
|
||||
@@ -4629,6 +4839,8 @@ int clip_n_mmproj_embd(const struct clip_ctx * ctx) {
|
||||
return ctx->model.qf_proj_blocks.size() * ctx->model.hparams.projection_dim;
|
||||
case PROJECTOR_TYPE_GLM4V:
|
||||
return ctx->model.mm_ffn_down_w->ne[1];
|
||||
case PROJECTOR_TYPE_MIMO_AUDIO:
|
||||
return ctx->model.mm_2_w->ne[1];
|
||||
default:
|
||||
GGML_ABORT("Unknown projector type");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
#include "models.h"
|
||||
|
||||
ggml_cgraph * clip_graph_mimo_audio::build() {
|
||||
ggml_tensor * inp = build_inp_raw(1); // [n_frames, n_mel, 1]
|
||||
|
||||
ggml_tensor * cur = ggml_conv_1d_ph(ctx0, model.conv1d_1_w, inp, 1, 1);
|
||||
cur = ggml_add(ctx0, cur, model.conv1d_1_b);
|
||||
cur = ggml_gelu_erf(ctx0, cur);
|
||||
|
||||
cur = ggml_conv_1d_ph(ctx0, model.conv1d_2_w, cur, 2, 1);
|
||||
cur = ggml_add(ctx0, cur, model.conv1d_2_b);
|
||||
cur = ggml_gelu_erf(ctx0, cur);
|
||||
|
||||
ggml_tensor * inpL = ggml_cont(ctx0, ggml_transpose(ctx0, cur)); // [n_embd, n_pos]
|
||||
const int64_t n_pos = inpL->ne[1];
|
||||
cb(inpL, "after_conv1d", -1);
|
||||
|
||||
GGML_ASSERT((int) hparams.wa_pattern_mode.size() == n_layer);
|
||||
|
||||
ggml_tensor * inp_pos = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_pos);
|
||||
ggml_set_name(inp_pos, "mimo_audio_positions");
|
||||
ggml_set_input(inp_pos);
|
||||
|
||||
ggml_tensor * full_mask = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, n_pos, n_pos);
|
||||
ggml_set_name(full_mask, "mimo_audio_full_mask");
|
||||
ggml_set_input(full_mask);
|
||||
|
||||
ggml_tensor * window_mask = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, n_pos, n_pos);
|
||||
ggml_set_name(window_mask, "mimo_audio_window_mask");
|
||||
ggml_set_input(window_mask);
|
||||
|
||||
build_vit_opts opts;
|
||||
opts.attn_mask_layers.resize(n_layer);
|
||||
for (int il = 0; il < n_layer; il++) {
|
||||
opts.attn_mask_layers[il] = hparams.wa_pattern_mode[il] == -1 ? full_mask : window_mask;
|
||||
}
|
||||
// the skip connection below must be added before the post-transformer norm,
|
||||
// so build_vit must not apply that norm itself
|
||||
opts.skip_post_ln = true;
|
||||
|
||||
// encoder_skip_layer_id=3 (1-indexed) -> capture output of layer index 2
|
||||
const int skip_capture_il = 2;
|
||||
GGML_ASSERT(n_layer > skip_capture_il);
|
||||
ggml_tensor * skip_hidden = nullptr;
|
||||
opts.callback_layer_out = [&](ggml_tensor * layer_cur, int il) {
|
||||
if (il == skip_capture_il) {
|
||||
skip_hidden = layer_cur;
|
||||
}
|
||||
};
|
||||
|
||||
auto add_pos = [&](ggml_tensor * x, const clip_layer &) {
|
||||
return ggml_rope_ext(ctx0, x, inp_pos, nullptr, d_head,
|
||||
GGML_ROPE_TYPE_NEOX, 0, hparams.rope_theta, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f);
|
||||
};
|
||||
|
||||
inpL = build_vit(inpL, n_pos, NORM_TYPE_NORMAL, hparams.ffn_op, nullptr, add_pos, opts);
|
||||
inpL = ggml_reshape_2d(ctx0, inpL, n_embd, n_pos); // build_vit restores a (size-1) batch dim
|
||||
|
||||
GGML_ASSERT(skip_hidden != nullptr);
|
||||
inpL = ggml_add(ctx0, inpL, skip_hidden);
|
||||
|
||||
inpL = build_norm(inpL, model.post_ln_w, model.post_ln_b, NORM_TYPE_NORMAL, eps, -1);
|
||||
cb(inpL, "after_transformer", -1);
|
||||
|
||||
// downsample: strided conv (no bias) + gelu + layernorm
|
||||
{
|
||||
ggml_tensor * ds = ggml_cont(ctx0, ggml_transpose(ctx0, inpL)); // [n_pos, n_embd]
|
||||
ds = ggml_conv_1d(ctx0, model.downsample_conv_w, ds, 2, 0, 1);
|
||||
ds = ggml_gelu_erf(ctx0, ds);
|
||||
ds = ggml_cont(ctx0, ggml_transpose(ctx0, ds)); // [n_embd, n_pos/2]
|
||||
ds = build_norm(ds, model.downsample_norm_w, model.downsample_norm_b, NORM_TYPE_NORMAL, eps, -1);
|
||||
inpL = ds;
|
||||
}
|
||||
cb(inpL, "after_downsample", -1);
|
||||
|
||||
// RVQ quantize: codebook ne=[dim, max_bins, n_q]
|
||||
// quantize input vector to codes (type=I32)
|
||||
std::vector<ggml_tensor *> codes;
|
||||
{
|
||||
GGML_ASSERT(model.rvq_codebook != nullptr);
|
||||
const int64_t dim = model.rvq_codebook->ne[0];
|
||||
GGML_ASSERT(dim == inpL->ne[0]);
|
||||
GGML_ASSERT((int64_t) hparams.rvq_codebook_size.size() == model.rvq_codebook->ne[2]);
|
||||
|
||||
ggml_tensor * residual = inpL; // [dim, n_pos_ds]
|
||||
|
||||
for (size_t q = 0; q < hparams.rvq_codebook_size.size(); q++) {
|
||||
const int64_t bins = hparams.rvq_codebook_size[q];
|
||||
ggml_tensor * codebook_q = ggml_view_2d(ctx0, model.rvq_codebook, dim, bins,
|
||||
model.rvq_codebook->nb[1], q * model.rvq_codebook->nb[2]);
|
||||
codebook_q = ggml_cont(ctx0, codebook_q);
|
||||
|
||||
ggml_tensor * codebook_norm = ggml_sum_rows(ctx0, ggml_sqr(ctx0, codebook_q)); // [1, bins]
|
||||
codebook_norm = ggml_cont(ctx0, ggml_transpose(ctx0, codebook_norm)); // [bins, 1]
|
||||
|
||||
ggml_tensor * dot = ggml_mul_mat(ctx0, codebook_q, residual); // [bins, n_pos_ds]
|
||||
ggml_tensor * scores = ggml_sub(ctx0, ggml_scale(ctx0, dot, 2.0f), codebook_norm);
|
||||
|
||||
ggml_tensor * idx = ggml_argmax(ctx0, scores); // [n_pos_ds]
|
||||
codes.push_back(idx);
|
||||
|
||||
ggml_tensor * quant = ggml_get_rows(ctx0, codebook_q, idx); // [dim, n_pos_ds]
|
||||
residual = ggml_sub(ctx0, residual, quant);
|
||||
cb(idx, "rvq_code", (int) q);
|
||||
}
|
||||
}
|
||||
|
||||
// convert codes to LLM embeddings
|
||||
ggml_tensor * code_embd_sum = nullptr;
|
||||
{
|
||||
GGML_ASSERT(model.mm_a_code_embd != nullptr);
|
||||
const int64_t dim = model.mm_a_code_embd->ne[0];
|
||||
const int64_t vocab = model.mm_a_code_embd->ne[1];
|
||||
GGML_ASSERT((int64_t) codes.size() == model.mm_a_code_embd->ne[2]);
|
||||
GGML_ASSERT(dim == inpL->ne[0]);
|
||||
|
||||
for (size_t i = 0; i < codes.size(); i++) {
|
||||
ggml_tensor * table_i = ggml_view_2d(ctx0, model.mm_a_code_embd, dim, vocab,
|
||||
model.mm_a_code_embd->nb[1], i * model.mm_a_code_embd->nb[2]);
|
||||
table_i = ggml_cont(ctx0, table_i);
|
||||
|
||||
ggml_tensor * embd_i = ggml_get_rows(ctx0, table_i, codes[i]); // [dim, n_pos_ds]
|
||||
code_embd_sum = code_embd_sum ? ggml_add(ctx0, code_embd_sum, embd_i) : embd_i;
|
||||
}
|
||||
cb(code_embd_sum, "code_embd_sum", -1);
|
||||
}
|
||||
|
||||
// input_local_transformer
|
||||
// groups of `group_size` consecutive downsampled frames are processed together, attending only within their own group.
|
||||
// Implemented as a block-diagonal mask + in-group-repeating positions
|
||||
// (rather than a real batch dim) - same technique as the encoder's masks above, and as gemma4a's / deepseekocr2's chunked attention.
|
||||
|
||||
// note: hand-rolled here instead of build_vit() because this is a second, independent layer stack
|
||||
// (own layer array/count, RMSNorm instead of LN, SiLU FFN, own RoPE theta)
|
||||
|
||||
ggml_tensor * projected;
|
||||
{
|
||||
const int group_size = hparams.audio_local_group_size;
|
||||
GGML_ASSERT(group_size > 0);
|
||||
const int64_t n_pos_ds = code_embd_sum->ne[1];
|
||||
const int64_t n_groups = (n_pos_ds + group_size - 1) / group_size;
|
||||
const int64_t n_padded = n_groups * group_size;
|
||||
|
||||
ggml_tensor * cur_local = code_embd_sum;
|
||||
if (n_padded != n_pos_ds) {
|
||||
cur_local = ggml_pad(ctx0, cur_local, 0, (int) (n_padded - n_pos_ds), 0, 0);
|
||||
}
|
||||
|
||||
ggml_tensor * local_pos = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_padded);
|
||||
ggml_set_name(local_pos, "mimo_audio_local_positions");
|
||||
ggml_set_input(local_pos);
|
||||
|
||||
ggml_tensor * local_mask = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, n_padded, n_padded);
|
||||
ggml_set_name(local_mask, "mimo_audio_local_mask");
|
||||
ggml_set_input(local_mask);
|
||||
|
||||
const float local_rope_theta = 640000.0f; // audio_config.rope_theta (differs from the encoder's)
|
||||
auto apply_local_rope = [&](ggml_tensor * x) {
|
||||
return ggml_rope_ext(ctx0, x, local_pos, nullptr, d_head,
|
||||
GGML_ROPE_TYPE_NEOX, 0, local_rope_theta, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f);
|
||||
};
|
||||
|
||||
for (int il = 0; il < hparams.audio_local_n_layer; il++) {
|
||||
auto & layer = model.mm_a_local_layers[il];
|
||||
|
||||
ggml_tensor * attn_in = build_norm(cur_local, layer.ln_1_w, nullptr, NORM_TYPE_RMS, eps, il);
|
||||
|
||||
ggml_tensor * Qcur = build_mm(layer.q_w, attn_in);
|
||||
if (layer.q_b) {
|
||||
Qcur = ggml_add(ctx0, Qcur, layer.q_b);
|
||||
}
|
||||
ggml_tensor * Kcur = build_mm(layer.k_w, attn_in);
|
||||
if (layer.k_b) {
|
||||
Kcur = ggml_add(ctx0, Kcur, layer.k_b);
|
||||
}
|
||||
ggml_tensor * Vcur = build_mm(layer.v_w, attn_in);
|
||||
if (layer.v_b) {
|
||||
Vcur = ggml_add(ctx0, Vcur, layer.v_b);
|
||||
}
|
||||
|
||||
Qcur = ggml_reshape_3d(ctx0, Qcur, d_head, n_head, n_padded);
|
||||
Kcur = ggml_reshape_3d(ctx0, Kcur, d_head, n_head, n_padded);
|
||||
Vcur = ggml_reshape_3d(ctx0, Vcur, d_head, n_head, n_padded);
|
||||
|
||||
Qcur = apply_local_rope(Qcur);
|
||||
Kcur = apply_local_rope(Kcur);
|
||||
|
||||
ggml_tensor * attn_out = build_attn(layer.o_w, nullptr, Qcur, Kcur, Vcur, local_mask, kq_scale, il);
|
||||
cur_local = ggml_add(ctx0, cur_local, attn_out);
|
||||
|
||||
ggml_tensor * ffn_in = build_norm(cur_local, layer.ln_2_w, nullptr, NORM_TYPE_RMS, eps, il);
|
||||
ggml_tensor * ffn_out = build_ffn(ffn_in,
|
||||
layer.ff_up_w, nullptr,
|
||||
layer.ff_gate_w, nullptr,
|
||||
layer.ff_down_w, nullptr,
|
||||
FFN_SILU, il);
|
||||
cur_local = ggml_add(ctx0, cur_local, ffn_out);
|
||||
}
|
||||
|
||||
cur_local = build_norm(cur_local, model.mm_a_local_norm_w, nullptr, NORM_TYPE_RMS, eps, -1);
|
||||
cb(cur_local, "after_local_transformer", -1);
|
||||
|
||||
// flatten each group of `group_size` frames into one (group_size*n_embd)-dim vector
|
||||
// (matching AudioProjection's flattened input)
|
||||
ggml_tensor * grouped = ggml_reshape_2d(ctx0, cur_local, n_embd * group_size, n_groups);
|
||||
|
||||
// AudioProjection: Linear (no bias) -> GELU -> Linear (no bias)
|
||||
projected = build_ffn(grouped,
|
||||
model.mm_1_w, nullptr,
|
||||
nullptr, nullptr,
|
||||
model.mm_2_w, nullptr,
|
||||
FFN_GELU_ERF, -1);
|
||||
cb(projected, "after_projection", -1);
|
||||
}
|
||||
|
||||
ggml_build_forward_expand(gf, projected);
|
||||
return gf;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "models.h"
|
||||
|
||||
ggml_tensor * clip_graph_minimax_m3::apply_rope(
|
||||
ggml_tensor * x, ggml_tensor * pos_h, ggml_tensor * pos_w) {
|
||||
const int64_t Hn = x->ne[1];
|
||||
const int64_t P = x->ne[2];
|
||||
const size_t es = ggml_element_size(x);
|
||||
const int dh = (int) x->ne[0];
|
||||
const int axd = 2 * ((2 * (dh / 2) / 3) / 2);
|
||||
|
||||
GGML_ASSERT(x->nb[0] == es);
|
||||
GGML_ASSERT(3 * axd <= dh);
|
||||
|
||||
const float th = hparams.rope_theta;
|
||||
|
||||
// layout of x is [t, h, w, pad]
|
||||
// t is unrotated, h and w are rotated, pad is unrotated
|
||||
// note: everything from n_dims onward untouched, so w and pad are rotated in one call.
|
||||
auto sl = [&](int off, int n) {
|
||||
return ggml_cont(ctx0, ggml_view_3d(ctx0, x, n, Hn, P, x->nb[1], x->nb[2], (size_t) off * es));
|
||||
};
|
||||
ggml_tensor * t = sl(0, axd);
|
||||
ggml_tensor * h = sl(axd, axd);
|
||||
ggml_tensor * w = sl(2 * axd, dh - 2 * axd); // w + pad
|
||||
|
||||
h = ggml_rope_ext(ctx0, h, pos_h, nullptr, axd, GGML_ROPE_TYPE_NEOX, 0, th, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f);
|
||||
w = ggml_rope_ext(ctx0, w, pos_w, nullptr, axd, GGML_ROPE_TYPE_NEOX, 0, th, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f);
|
||||
return ggml_concat(ctx0, ggml_concat(ctx0, t, h, 0), w, 0);
|
||||
}
|
||||
|
||||
ggml_cgraph * clip_graph_minimax_m3::build() {
|
||||
GGML_ASSERT(model.patch_bias == nullptr);
|
||||
GGML_ASSERT(model.class_embedding == nullptr);
|
||||
GGML_ASSERT(model.patch_embeddings_0 && model.patch_embeddings_1);
|
||||
GGML_ASSERT(model.mm_1_w && model.mm_2_w);
|
||||
GGML_ASSERT(model.mm_merger_fc1_w && model.mm_merger_fc2_w);
|
||||
|
||||
const int batch_size = 1;
|
||||
const int n_pos = n_patches;
|
||||
const int merge = hparams.n_merge;
|
||||
|
||||
// patch embedding
|
||||
ggml_tensor * inp_raw = build_inp_raw();
|
||||
ggml_tensor * inp = ggml_add(ctx0,
|
||||
ggml_conv_2d(ctx0, model.patch_embeddings_0, inp_raw, patch_size, patch_size, 0, 0, 1, 1),
|
||||
ggml_conv_2d(ctx0, model.patch_embeddings_1, inp_raw, patch_size, patch_size, 0, 0, 1, 1));
|
||||
|
||||
// spatial merge
|
||||
{
|
||||
inp = ggml_permute(ctx0, inp, 1, 2, 0, 3);
|
||||
inp = ggml_cont_4d(ctx0, inp, n_embd * merge, n_patches_x / merge, n_patches_y, batch_size);
|
||||
inp = ggml_reshape_4d(ctx0, inp, n_embd * merge, n_patches_x / merge, merge, batch_size * (n_patches_y / merge));
|
||||
inp = ggml_permute(ctx0, inp, 0, 2, 1, 3);
|
||||
inp = ggml_cont_3d(ctx0, inp, n_embd, n_patches_x * n_patches_y, batch_size);
|
||||
}
|
||||
|
||||
// t (time axis) is always 0 for now, so we leave it unrotated
|
||||
ggml_tensor * pos_h = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_pos);
|
||||
ggml_set_name(pos_h, "minimax_pos_h"); ggml_set_input(pos_h);
|
||||
ggml_tensor * pos_w = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_pos);
|
||||
ggml_set_name(pos_w, "minimax_pos_w"); ggml_set_input(pos_w);
|
||||
|
||||
ggml_tensor * inpL = build_vit(
|
||||
inp, n_pos, NORM_TYPE_NORMAL, FFN_GELU_ERF, nullptr,
|
||||
[&](ggml_tensor * c, const clip_layer &) {
|
||||
return apply_rope(c, pos_h, pos_w);
|
||||
});
|
||||
|
||||
// projector
|
||||
ggml_tensor * emb = inpL;
|
||||
emb = build_ffn(emb, model.mm_1_w, model.mm_1_b,
|
||||
nullptr, nullptr,
|
||||
model.mm_2_w, model.mm_2_b, FFN_GELU_ERF, -1);
|
||||
|
||||
const int64_t proj = emb->ne[0];
|
||||
emb = ggml_reshape_2d(ctx0, emb, proj * merge * merge, n_pos / (merge * merge));
|
||||
|
||||
emb = build_ffn(emb, model.mm_merger_fc1_w, model.mm_merger_fc1_b,
|
||||
nullptr, nullptr,
|
||||
model.mm_merger_fc2_w, model.mm_merger_fc2_b, FFN_GELU_ERF, -1);
|
||||
|
||||
ggml_build_forward_expand(gf, emb);
|
||||
return gf;
|
||||
}
|
||||
@@ -40,6 +40,12 @@ struct clip_graph_qwen3vl : clip_graph_qwen2vl {
|
||||
ggml_cgraph * build() override;
|
||||
};
|
||||
|
||||
struct clip_graph_minimax_m3 : clip_graph {
|
||||
clip_graph_minimax_m3(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
|
||||
ggml_cgraph * build() override;
|
||||
ggml_tensor * apply_rope(ggml_tensor * x, ggml_tensor * pos_h, ggml_tensor * pos_w);
|
||||
};
|
||||
|
||||
struct clip_graph_mimovl : clip_graph {
|
||||
clip_graph_mimovl(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
|
||||
ggml_cgraph * build() override;
|
||||
@@ -204,6 +210,11 @@ struct clip_graph_qwen3a : clip_graph {
|
||||
ggml_cgraph * build() override;
|
||||
};
|
||||
|
||||
struct clip_graph_mimo_audio : clip_graph {
|
||||
clip_graph_mimo_audio(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
|
||||
ggml_cgraph * build() override;
|
||||
};
|
||||
|
||||
struct clip_graph_kimik25 : clip_graph {
|
||||
clip_graph_kimik25(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
|
||||
ggml_cgraph * build() override;
|
||||
|
||||
@@ -725,6 +725,72 @@ bool mtmd_audio_preprocessor_qwen3a::preprocess(const float * sa
|
||||
return true;
|
||||
}
|
||||
|
||||
//
|
||||
// mtmd_audio_preprocessor_mimo_audio
|
||||
//
|
||||
// Matches torchaudio.transforms.MelSpectrogram(power=1.0, center=True) followed by
|
||||
// log(clip(spec, min=1e-7)): HTK mel scale, no Slaney area norm, magnitude (not power)
|
||||
// spectrogram, natural log, reflect-padded by n_fft/2 on each side.
|
||||
//
|
||||
|
||||
void mtmd_audio_preprocessor_mimo_audio::initialize() {
|
||||
cache.fill_sin_cos_table(hparams.audio_n_fft);
|
||||
cache.fill_hann_window(hparams.audio_window_len, true);
|
||||
cache.fill_mel_filterbank_matrix(
|
||||
hparams.n_mel_bins, hparams.audio_n_fft, hparams.audio_sample_rate,
|
||||
0.0f, hparams.audio_sample_rate / 2.0f,
|
||||
/*slaney_area_norm=*/ false,
|
||||
/*scale=*/ 1.0f,
|
||||
/*use_htk=*/ true
|
||||
);
|
||||
}
|
||||
|
||||
bool mtmd_audio_preprocessor_mimo_audio::preprocess(const float * samples,
|
||||
size_t n_samples,
|
||||
std::vector<mtmd_audio_mel> & output) {
|
||||
if (n_samples == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
GGML_ASSERT(!cache.sin_vals.empty());
|
||||
GGML_ASSERT(!cache.cos_vals.empty());
|
||||
GGML_ASSERT(!cache.filters.data.empty());
|
||||
|
||||
const int pad = hparams.audio_n_fft / 2;
|
||||
|
||||
std::vector<float> padded(n_samples + 2 * pad, 0.0f);
|
||||
for (int i = 0; i < pad; i++) {
|
||||
int src = pad - i;
|
||||
padded[i] = (src < (int)n_samples) ? samples[src] : 0.0f;
|
||||
}
|
||||
std::copy(samples, samples + n_samples, padded.begin() + pad);
|
||||
for (int i = 0; i < pad; i++) {
|
||||
int src = (int)n_samples - 2 - i;
|
||||
padded[n_samples + pad + i] = (src >= 0) ? samples[src] : 0.0f;
|
||||
}
|
||||
|
||||
filter_params params;
|
||||
params.n_mel = hparams.n_mel_bins;
|
||||
params.n_fft_bins = 1 + (hparams.audio_n_fft / 2);
|
||||
params.hann_window_size = hparams.audio_window_len;
|
||||
params.hop_length = hparams.audio_hop_len;
|
||||
params.sample_rate = hparams.audio_sample_rate;
|
||||
params.no_padding = true; // reflect padding already applied above
|
||||
params.use_natural_log = true;
|
||||
params.use_magnitude = true;
|
||||
params.mel_floor = 1e-7f;
|
||||
params.norm_per_feature = false;
|
||||
|
||||
mtmd_audio_mel out;
|
||||
bool ok = log_mel_spectrogram(padded.data(), (int)padded.size(), 4, params, cache, out);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
output.push_back(std::move(out));
|
||||
return true;
|
||||
}
|
||||
|
||||
//
|
||||
// mtmd_audio_preprocessor_conformer
|
||||
//
|
||||
|
||||
@@ -111,6 +111,15 @@ struct mtmd_audio_preprocessor_qwen3a : mtmd_audio_preprocessor {
|
||||
mtmd_audio_cache cache;
|
||||
};
|
||||
|
||||
struct mtmd_audio_preprocessor_mimo_audio : mtmd_audio_preprocessor {
|
||||
mtmd_audio_preprocessor_mimo_audio(const clip_ctx * ctx) : mtmd_audio_preprocessor(ctx) {}
|
||||
void initialize() override;
|
||||
bool preprocess(const float * samples, size_t n_samples, std::vector<mtmd_audio_mel> & output) override;
|
||||
|
||||
private:
|
||||
mtmd_audio_cache cache;
|
||||
};
|
||||
|
||||
//
|
||||
// streaming ISTFT - converts spectrogram frames back to audio one frame at a time
|
||||
//
|
||||
|
||||
@@ -640,6 +640,7 @@ bool mtmd_helper_support_video(mtmd_context * ctx) {
|
||||
#ifdef MTMD_VIDEO
|
||||
return mtmd_support_vision(ctx);
|
||||
#else
|
||||
GGML_UNUSED(ctx);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
@@ -1007,6 +1008,9 @@ mtmd_helper_video * mtmd_helper_video_init(
|
||||
|
||||
return ctx;
|
||||
#else
|
||||
GGML_UNUSED(mctx);
|
||||
GGML_UNUSED(path);
|
||||
GGML_UNUSED(params);
|
||||
LOG_ERR("%s: video is not supported in this build (MTMD_VIDEO is set to OFF)\n", __func__);
|
||||
return nullptr;
|
||||
#endif
|
||||
@@ -1039,6 +1043,10 @@ mtmd_helper_video * mtmd_helper_video_init_from_buf(
|
||||
|
||||
return ctx;
|
||||
#else
|
||||
GGML_UNUSED(mctx);
|
||||
GGML_UNUSED(buf);
|
||||
GGML_UNUSED(len);
|
||||
GGML_UNUSED(params);
|
||||
LOG_ERR("%s: video is not supported in this build (MTMD_VIDEO is set to OFF)\n", __func__);
|
||||
return nullptr;
|
||||
#endif
|
||||
@@ -1050,6 +1058,7 @@ void mtmd_helper_video_free(mtmd_helper_video * ctx) {
|
||||
ctx->stop_ffmpeg();
|
||||
delete ctx;
|
||||
#else
|
||||
GGML_UNUSED(ctx);
|
||||
LOG_ERR("%s: video is not supported in this build (MTMD_VIDEO is set to OFF)\n", __func__);
|
||||
#endif
|
||||
}
|
||||
@@ -1058,6 +1067,7 @@ mtmd_helper_video_info mtmd_helper_video_get_info(const mtmd_helper_video * ctx)
|
||||
#ifdef MTMD_VIDEO
|
||||
return ctx->info;
|
||||
#else
|
||||
GGML_UNUSED(ctx);
|
||||
GGML_ASSERT(false && "video is not supported in this build (MTMD_VIDEO is set to OFF)");
|
||||
#endif
|
||||
}
|
||||
@@ -1068,6 +1078,9 @@ int32_t mtmd_helper_video_read_next(mtmd_helper_video * ctx,
|
||||
if (!ctx) return -2;
|
||||
return ctx->read_next(out_bitmap, out_text);
|
||||
#else
|
||||
GGML_UNUSED(ctx);
|
||||
GGML_UNUSED(out_bitmap);
|
||||
GGML_UNUSED(out_text);
|
||||
GGML_ASSERT(false && "video is not supported in this build (MTMD_VIDEO is set to OFF)");
|
||||
#endif
|
||||
}
|
||||
|
||||
+24
-3
@@ -463,6 +463,13 @@ struct mtmd_context {
|
||||
img_end = "<|vision_end|>";
|
||||
image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_MINIMAX_M3:
|
||||
{
|
||||
// ]<]start of image[>[ ... (image embeddings) ... ]<]end of image[>[
|
||||
img_beg = "]<]start of image[>[";
|
||||
img_end = "]<]end of image[>[";
|
||||
image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_YOUTUVL:
|
||||
{
|
||||
// <|vision_start|> ... (image embeddings) ... <|vision_end|>
|
||||
@@ -555,9 +562,17 @@ struct mtmd_context {
|
||||
} break;
|
||||
case PROJECTOR_TYPE_KIMIK25:
|
||||
{
|
||||
// <|media_begin|> ... (image embeddings) ... <|media_end|>
|
||||
img_beg = "<|media_begin|>";
|
||||
img_end = "<|media_end|>";
|
||||
// GLM-5.2-V reuses the Kimi-K2.5 vision encoder and projector, but marks
|
||||
// images with its own tokens, so decide based on the text model vocab
|
||||
if (lookup_token("<|begin_of_image|>") != LLAMA_TOKEN_NULL) {
|
||||
// <|begin_of_image|> ... (image embeddings) ... <|end_of_image|>
|
||||
img_beg = "<|begin_of_image|>";
|
||||
img_end = "<|end_of_image|>";
|
||||
} else {
|
||||
// <|media_begin|> ... (image embeddings) ... <|media_end|>
|
||||
img_beg = "<|media_begin|>";
|
||||
img_end = "<|media_end|>";
|
||||
}
|
||||
image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_LIGHTONOCR:
|
||||
@@ -715,6 +730,12 @@ struct mtmd_context {
|
||||
aud_end = "<audio|>";
|
||||
audio_preproc = std::make_unique<mtmd_audio_preprocessor_gemma4ua>(ctx_a);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_MIMO_AUDIO:
|
||||
{
|
||||
aud_beg = "<|mimo_audio_start|>";
|
||||
aud_end = "<|mimo_audio_end|>";
|
||||
audio_preproc = std::make_unique<mtmd_audio_preprocessor_mimo_audio>(ctx_a);
|
||||
} break;
|
||||
default:
|
||||
throw std::runtime_error(string_format("%s: unexpected audio projector type %d\n", __func__, proj));
|
||||
}
|
||||
|
||||
@@ -136,13 +136,13 @@ Producer side: `server_res_generator` extends `server_res_spipe`, which keeps al
|
||||
|
||||
Lifetime safety: the session holds no back reference to the response, so `spipe` is a plain `unique_ptr` touched only by the http worker. `cancel` raises an atomic the producer polls; the producer finalizes the session from its destructor, which also runs `~server_response_reader::stop()` to cancel the generation at the queue level. A `DELETE` stops work by raising the flag and letting the worker unwind.
|
||||
|
||||
Consumer side: `GET /v1/stream/<conv_id>?from=N` opens a `text/event-stream` that replays buffered bytes from offset `N` and blocks for live bytes, so the browser reattaches like a fresh EventSource. An offset below the dropped prefix returns 400.
|
||||
Consumer side: `GET /v1/stream?conv_id=<id>&from=N` opens a `text/event-stream` that replays buffered bytes from offset `N` and blocks for live bytes, so the browser reattaches like a fresh EventSource. An offset below the dropped prefix returns 400.
|
||||
|
||||
Routes:
|
||||
|
||||
- `GET /v1/stream/:conv_id?from=N`: replay or live reattach.
|
||||
- `GET /v1/stream?conv_id=<id>&from=N`: replay or live reattach. The id travels in the query string because it can embed a model name containing slashes.
|
||||
- `POST /v1/streams/lookup` with `{"conversation_ids": [...]}`: returns session status only for ids the caller already owns. There is no listing route, so live sessions cannot be enumerated (an earlier `GET /v1/streams` was removed for exactly this reason).
|
||||
- `DELETE /v1/stream/:conv_id`: explicit Stop, idempotent (`evict_and_cancel`).
|
||||
- `DELETE /v1/stream?conv_id=<id>`: explicit Stop, idempotent (`evict_and_cancel`).
|
||||
|
||||
Router mode binds the same paths to proxy handlers. A `conv_id -> child` map (`conv_models`), populated when a POST is routed, resolves the owning child in one lookup with no polling. The lookup groups ids per child; GET and DELETE proxy straight to the owner. This loopback REST hop is expected to move to a websocket IPC later, swapping only the transport.
|
||||
|
||||
@@ -166,8 +166,8 @@ graph TD
|
||||
GC[GC thread] -- drop after TTL --> Sess
|
||||
end
|
||||
Sess -- read_from offset --> Cons[stream_pipe_consumer]
|
||||
Cons -- "GET /v1/stream/:id?from=N" --> Client
|
||||
DEL[DELETE /v1/stream/:id] -- evict_and_cancel --> Sess
|
||||
Cons -- "GET /v1/stream?conv_id=id&from=N" --> Client
|
||||
DEL[DELETE /v1/stream?conv_id=id] -- evict_and_cancel --> Sess
|
||||
```
|
||||
|
||||
The diagram shows the buffer touch points. The live wire (chunks streamed to the original client during a normal generation) is the producer's default output, described under "Producer side" above.
|
||||
|
||||
@@ -72,10 +72,10 @@ For the full list of features, please refer to [server's changelog](https://gith
|
||||
| `-ctv, --cache-type-v TYPE` | KV cache data type for V<br/>allowed values: f32, f16, bf16, q8_0, q4_0, q4_1, iq4_nl, q5_0, q5_1<br/>(default: f16)<br/>(env: LLAMA_ARG_CACHE_TYPE_V) |
|
||||
| `-dt, --defrag-thold N` | KV cache defragmentation threshold (DEPRECATED)<br/>(env: LLAMA_ARG_DEFRAG_THOLD) |
|
||||
| `--rpc SERVERS` | comma-separated list of RPC servers (host:port)<br/>(env: LLAMA_ARG_RPC) |
|
||||
| `--mlock` | DEPRECATED in favor of `--load-mode`: mmap + force system to keep model in RAM rather than swapping or compressing<br/>(env: LLAMA_ARG_MLOCK) |
|
||||
| `--mlock` | DEPRECATED in favor of `--load-mode`: force system to keep model in RAM rather than swapping or compressing<br/>(env: LLAMA_ARG_MLOCK) |
|
||||
| `--mmap, --no-mmap` | DEPRECATED in favor of `--load-mode`: whether to memory-map model. (if mmap disabled, slower load but may reduce pageouts if not using mlock)<br/>(env: LLAMA_ARG_MMAP) |
|
||||
| `-dio, --direct-io, -ndio, --no-direct-io` | DEPRECATED in favor of `--load-mode`: use DirectIO if available<br/>(env: LLAMA_ARG_DIO) |
|
||||
| `-lm, --load-mode MODE` | model loading mode (default: mmap)<br/>- none: no special loading mode<br/>- mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)<br/>- mlock: mmap + force system to keep model in RAM rather than swapping or compressing<br/>- dio: use DirectIO if available<br/><br/>(env: LLAMA_ARG_LOAD_MODE) |
|
||||
| `-lm, --load-mode MODE` | model loading mode (default: mmap)<br/>- none: no special loading mode<br/>- mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)<br/>- mlock: force system to keep model in RAM rather than swapping or compressing<br/>- mmap+mlock: mmap + force system to keep model in RAM rather than swapping or compressing<br/>- dio: use DirectIO if available<br/><br/>(env: LLAMA_ARG_LOAD_MODE) |
|
||||
| `--numa TYPE` | attempt optimizations that help on some NUMA systems<br/>- distribute: spread execution evenly over all nodes<br/>- isolate: only spawn threads on CPUs on the node that execution started on<br/>- numactl: use the CPU map provided by numactl<br/>if run without this previously, it is recommended to drop the system page cache before using this<br/>see https://github.com/ggml-org/llama.cpp/issues/1437<br/>(env: LLAMA_ARG_NUMA) |
|
||||
| `-dev, --device <dev1,dev2,..>` | comma-separated list of devices to use for offloading (none = don't offload)<br/>use --list-devices to see a list of available devices<br/>(env: LLAMA_ARG_DEVICE) |
|
||||
| `--list-devices` | print list of available devices and exit |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "server-mcp.h"
|
||||
|
||||
#include <sheredom/subprocess.h>
|
||||
#include "subproc.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
@@ -341,7 +341,7 @@ json server_mcp_transport::call_tool(const std::string & tool_name,
|
||||
//
|
||||
|
||||
struct server_mcp_stdio::process_handle {
|
||||
subprocess_s sp;
|
||||
common_subproc sp;
|
||||
FILE * in = nullptr; // child stdin
|
||||
FILE * out = nullptr; // child stdout
|
||||
FILE * err = nullptr; // child stderr
|
||||
@@ -483,30 +483,15 @@ bool server_mcp_stdio::start() {
|
||||
envp_s = mcp_build_env(config.env);
|
||||
}
|
||||
|
||||
auto to_ptrs = [](std::vector<std::string> & v) {
|
||||
std::vector<const char *> p;
|
||||
p.reserve(v.size() + 1);
|
||||
for (auto & s : v) {
|
||||
p.push_back(s.c_str());
|
||||
}
|
||||
p.push_back(nullptr);
|
||||
return p;
|
||||
};
|
||||
auto argv = to_ptrs(argv_s);
|
||||
auto envp = to_ptrs(envp_s);
|
||||
|
||||
auto handle = std::make_unique<process_handle>();
|
||||
int rc = subprocess_create_ex(argv.data(), options,
|
||||
config.env.empty() ? nullptr : envp.data(),
|
||||
config.cwd.empty() ? nullptr : config.cwd.c_str(),
|
||||
&handle->sp);
|
||||
if (rc != 0) {
|
||||
bool ok = handle->sp.create(argv_s, options, envp_s, config.cwd.empty() ? nullptr : config.cwd.c_str());
|
||||
if (!ok) {
|
||||
SRV_WRN("MCP '%s': failed to spawn '%s'\n", config.name.c_str(), config.command.c_str());
|
||||
return false;
|
||||
}
|
||||
handle->in = subprocess_stdin(&handle->sp);
|
||||
handle->out = subprocess_stdout(&handle->sp);
|
||||
handle->err = subprocess_stderr(&handle->sp);
|
||||
handle->in = handle->sp.stdin_file();
|
||||
handle->out = handle->sp.stdout_file();
|
||||
handle->err = handle->sp.stderr_file();
|
||||
|
||||
proc = std::move(handle);
|
||||
running.store(true);
|
||||
@@ -654,14 +639,13 @@ void server_mcp_stdio::join_pumps() {
|
||||
to_server.close_write(); // wake the writer if it waits for a message
|
||||
from_server.close_write(); // wake any caller waiting for a reply
|
||||
|
||||
subprocess_terminate(&proc->sp); // child death unblocks the blocked fread/fwrite
|
||||
proc->sp.terminate(); // child death unblocks the blocked fread/fwrite
|
||||
|
||||
if (writer.joinable()) writer.join();
|
||||
if (reader.joinable()) reader.join();
|
||||
if (errlog.joinable()) errlog.join();
|
||||
|
||||
subprocess_join(&proc->sp, nullptr); // reap the child: destroy() never waits, so the pid would stay a zombie for the process lifetime
|
||||
subprocess_destroy(&proc->sp); // safe now: no thread touches the FILE* anymore
|
||||
proc->sp.join(); // reap the child: never waiting would leave the pid a zombie for the process lifetime
|
||||
proc.reset();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
#include "preset.h"
|
||||
#include "download.h"
|
||||
#include "http.h"
|
||||
#include "subproc.h"
|
||||
|
||||
#include <cpp-httplib/httplib.h> // TODO: remove this once we use HTTP client from download.h
|
||||
#include <optional>
|
||||
#include <sheredom/subprocess.h>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
@@ -49,43 +49,24 @@ extern char **environ;
|
||||
#define CHILD_ADDR "127.0.0.1"
|
||||
|
||||
struct server_subproc {
|
||||
std::optional<subprocess_s> sproc; // empty while in DOWNLOADING state
|
||||
common_subproc sproc; // not yet spawned while in DOWNLOADING state
|
||||
std::atomic<bool> stopped{false}; // set to cancel a download or signal child process exit
|
||||
|
||||
subprocess_s & get() {
|
||||
GGML_ASSERT(sproc.has_value() && "subprocess not initialized");
|
||||
return sproc.value();
|
||||
}
|
||||
|
||||
bool is_alive() {
|
||||
return sproc.has_value() && subprocess_alive(&sproc.value());
|
||||
return sproc.alive();
|
||||
}
|
||||
|
||||
void request_exit() {
|
||||
if (sproc.has_value()) {
|
||||
FILE * stdin_file = subprocess_stdin(&sproc.value());
|
||||
if (stdin_file) {
|
||||
fprintf(stdin_file, "%s\n", CMD_ROUTER_TO_CHILD_EXIT);
|
||||
fflush(stdin_file);
|
||||
}
|
||||
FILE * stdin_file = sproc.stdin_file();
|
||||
if (stdin_file) {
|
||||
fprintf(stdin_file, "%s\n", CMD_ROUTER_TO_CHILD_EXIT);
|
||||
fflush(stdin_file);
|
||||
}
|
||||
stopped.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void terminate() {
|
||||
if (!sproc.has_value()) {
|
||||
return;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
if (sproc->hProcess == NULL) {
|
||||
return;
|
||||
}
|
||||
#else
|
||||
if (sproc->child <= 0) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
subprocess_terminate(&sproc.value());
|
||||
sproc.terminate();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -711,18 +692,6 @@ std::optional<server_model_meta> server_models::get_meta(const std::string & nam
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// helper to convert vector<string> to char **
|
||||
// pointers are only valid as long as the original vector is valid
|
||||
static std::vector<char *> to_char_ptr_array(const std::vector<std::string> & vec) {
|
||||
std::vector<char *> result;
|
||||
result.reserve(vec.size() + 1);
|
||||
for (const auto & s : vec) {
|
||||
result.push_back(const_cast<char*>(s.c_str()));
|
||||
}
|
||||
result.push_back(nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<server_model_meta> server_models::get_all_meta() {
|
||||
std::unique_lock<std::mutex> lk(mutex);
|
||||
if (need_reload) {
|
||||
@@ -845,15 +814,10 @@ void server_models::load(const std::string & name, const load_options & opts) {
|
||||
}
|
||||
inst.meta.args = child_args; // save for debugging
|
||||
|
||||
std::vector<char *> argv = to_char_ptr_array(child_args);
|
||||
std::vector<char *> envp = to_char_ptr_array(child_env);
|
||||
|
||||
// TODO @ngxson : maybe separate stdout and stderr in the future
|
||||
// so that we can use stdout for commands and stderr for logging
|
||||
int options = subprocess_option_no_window | subprocess_option_combined_stdout_stderr;
|
||||
inst.subproc->sproc.emplace();
|
||||
int result = subprocess_create_ex(argv.data(), options, envp.data(), nullptr, &inst.subproc->get());
|
||||
if (result != 0) {
|
||||
if (!inst.subproc->sproc.create(child_args, options, child_env)) {
|
||||
throw std::runtime_error("failed to spawn server instance");
|
||||
}
|
||||
}
|
||||
@@ -867,8 +831,8 @@ void server_models::load(const std::string & name, const load_options & opts) {
|
||||
stop_timeout = inst.meta.stop_timeout,
|
||||
child_mode = opts.mode
|
||||
]() {
|
||||
FILE * stdin_file = subprocess_stdin(&child_proc->get());
|
||||
FILE * stdout_file = subprocess_stdout(&child_proc->get()); // combined stdout/stderr
|
||||
FILE * stdin_file = child_proc->sproc.stdin_file();
|
||||
FILE * stdout_file = child_proc->sproc.stdout_file(); // combined stdout/stderr
|
||||
|
||||
std::thread log_thread([&]() {
|
||||
// read stdout/stderr and forward to main server log
|
||||
@@ -942,9 +906,7 @@ void server_models::load(const std::string & name, const load_options & opts) {
|
||||
}
|
||||
|
||||
// get the exit code
|
||||
int exit_code = 0;
|
||||
subprocess_join(&child_proc->get(), &exit_code);
|
||||
subprocess_destroy(&child_proc->get());
|
||||
int exit_code = child_proc->sproc.join();
|
||||
|
||||
// update status and exit code
|
||||
if (child_mode == SERVER_CHILD_MODE_DOWNLOAD) {
|
||||
@@ -1210,7 +1172,7 @@ bool server_models::ensure_model_ready(const std::string & name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
server_http_res_ptr server_models::proxy_request(const server_http_req & req, const std::string & method, const std::string & name, bool update_last_used) {
|
||||
server_http_res_ptr server_models::proxy_request(const server_http_req & req, const std::string & method, const std::string & name, bool update_last_used, bool detached) {
|
||||
auto meta = get_meta(name);
|
||||
if (!meta.has_value()) {
|
||||
throw std::runtime_error("model name=" + name + " is not found");
|
||||
@@ -1236,7 +1198,10 @@ server_http_res_ptr server_models::proxy_request(const server_http_req & req, co
|
||||
req.headers,
|
||||
req.body,
|
||||
req.files,
|
||||
req.should_stop,
|
||||
// a detached request belongs to a replay session that outlives the client socket:
|
||||
// it reaches the child even when the downstream died during the load wait, the
|
||||
// session buffer is the recipient and DELETE remains the stop
|
||||
detached ? std::function<bool()>([]() { return false; }) : req.should_stop,
|
||||
base_params.timeout_read,
|
||||
base_params.timeout_write
|
||||
);
|
||||
@@ -1507,13 +1472,9 @@ static bool router_validate_model(std::string & name, server_models & models, bo
|
||||
}
|
||||
// resolve alias to canonical model name
|
||||
name = meta->name;
|
||||
if (models_autoload) {
|
||||
models.ensure_model_ready(name);
|
||||
} else {
|
||||
if (!meta->is_running()) {
|
||||
res_err(res, format_error_response("model is not loaded", ERROR_TYPE_INVALID_REQUEST));
|
||||
return false;
|
||||
}
|
||||
if (!models_autoload && !meta->is_running()) {
|
||||
res_err(res, format_error_response("model is not loaded", ERROR_TYPE_INVALID_REQUEST));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1567,6 +1528,10 @@ static std::optional<server_model_meta> resolve_child_for_conv(
|
||||
}
|
||||
|
||||
void server_models_routes::init_routes() {
|
||||
if (!common_subproc::is_supported()) {
|
||||
throw std::runtime_error("subprocess is not enabled on this build");
|
||||
}
|
||||
|
||||
this->get_router_props = [this](const server_http_req & req) {
|
||||
std::string name = req.get_param("model");
|
||||
if (name.empty()) {
|
||||
@@ -1602,6 +1567,9 @@ void server_models_routes::init_routes() {
|
||||
if (!router_validate_model(name, models, autoload, error_res)) {
|
||||
return error_res;
|
||||
}
|
||||
if (autoload) {
|
||||
models.ensure_model_ready(name);
|
||||
}
|
||||
return models.proxy_request(req, method, name, false);
|
||||
};
|
||||
|
||||
@@ -1615,12 +1583,23 @@ void server_models_routes::init_routes() {
|
||||
return error_res;
|
||||
}
|
||||
// remember which child serves this conversation so the stream routes can route straight
|
||||
// to it without polling, keyed on the exact conv id from the header
|
||||
// to it without polling, keyed on the exact conv id from the header. registered before
|
||||
// the load wait so a stop issued while the model loads can erase the entry and cancel
|
||||
// this request instead of leaving an orphan generation
|
||||
std::string conv_id = server_stream_conv_id_from_headers(req.headers);
|
||||
if (!conv_id.empty()) {
|
||||
models.conv_models.remember(conv_id, name);
|
||||
uint64_t ticket = models.conv_models.remember(conv_id, name);
|
||||
bool waited = autoload && models.ensure_model_ready(name);
|
||||
if (ticket != 0 && !models.conv_models.alive(conv_id, ticket)) {
|
||||
SRV_INF("request for conv_id=%s cancelled while model name=%s was loading\n",
|
||||
conv_id.c_str(), name.c_str());
|
||||
res_err(error_res, format_error_response(
|
||||
"request cancelled by a stop while the model was loading", ERROR_TYPE_INVALID_REQUEST));
|
||||
return error_res;
|
||||
}
|
||||
return models.proxy_request(req, method, name, true); // update last usage for POST request only
|
||||
// a session request that waited for a load detaches from the client socket: the
|
||||
// client may have dropped during the wait (page reload) and the session buffer must
|
||||
// still receive the generation for a later resume
|
||||
return models.proxy_request(req, method, name, true, waited && ticket != 0); // update last usage for POST request only
|
||||
};
|
||||
|
||||
this->post_router_models_load = [this](const server_http_req & req) {
|
||||
@@ -1813,7 +1792,7 @@ void server_models_routes::init_routes() {
|
||||
};
|
||||
|
||||
this->router_stream_get = [this](const server_http_req & req) {
|
||||
// GET /v1/stream/<conv_id>?from=N. resolve the owning child from the conv_id -> model
|
||||
// GET /v1/stream?conv_id=<id>&from=N. resolve the owning child from the conv_id -> model
|
||||
// map, 404 when nothing maps
|
||||
auto res = std::make_unique<server_http_res>();
|
||||
std::string conv_id = req.get_param("conv_id");
|
||||
@@ -1823,13 +1802,24 @@ void server_models_routes::init_routes() {
|
||||
}
|
||||
std::optional<server_model_meta> owner = resolve_child_for_conv(models, conv_id);
|
||||
if (!owner.has_value()) {
|
||||
res_err(res, format_error_response("Stream not found or expired", ERROR_TYPE_NOT_FOUND));
|
||||
// a registered conv whose model is still loading earns a retry: the session appears
|
||||
// once the load ends and the pending request reaches the child
|
||||
auto tracked = models.conv_models.lookup(conv_id);
|
||||
auto meta = tracked.has_value() ? models.get_meta(*tracked) : std::nullopt;
|
||||
bool transient = meta.has_value() && (meta->status == SERVER_MODEL_STATUS_LOADING ||
|
||||
meta->status == SERVER_MODEL_STATUS_DOWNLOADING ||
|
||||
meta->status == SERVER_MODEL_STATUS_DOWNLOADED);
|
||||
if (transient) {
|
||||
res_err(res, format_error_response("Stream owner model is loading, retry later", ERROR_TYPE_UNAVAILABLE));
|
||||
} else {
|
||||
res_err(res, format_error_response("Stream not found or expired", ERROR_TYPE_NOT_FOUND));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
std::string from = req.get_param("from");
|
||||
std::string child_path = "/v1/stream/" + encode_qs(conv_id);
|
||||
std::string child_path = "/v1/stream?conv_id=" + encode_qs(conv_id);
|
||||
if (!from.empty()) {
|
||||
child_path += "?from=" + from;
|
||||
child_path += "&from=" + from;
|
||||
}
|
||||
SRV_TRC("proxying stream resume to model %s on port %d, path=%s\n",
|
||||
owner->name.c_str(), owner->port, child_path.c_str());
|
||||
@@ -1909,7 +1899,7 @@ void server_models_routes::init_routes() {
|
||||
};
|
||||
|
||||
this->router_stream_delete = [this](const server_http_req & req) {
|
||||
// DELETE /v1/stream/<conv_id>. resolve the owning child via the map and forward only to
|
||||
// DELETE /v1/stream?conv_id=<id>. resolve the owning child via the map and forward only to
|
||||
// it, evict_and_cancel is idempotent on the child
|
||||
auto res = std::make_unique<server_http_res>();
|
||||
std::string conv_id = req.get_param("conv_id");
|
||||
@@ -1917,7 +1907,7 @@ void server_models_routes::init_routes() {
|
||||
res_err(res, format_error_response("Missing conversation id in path", ERROR_TYPE_INVALID_REQUEST));
|
||||
return res;
|
||||
}
|
||||
std::string child_path = "/v1/stream/" + encode_qs(conv_id);
|
||||
std::string child_path = "/v1/stream?conv_id=" + encode_qs(conv_id);
|
||||
auto owner = resolve_child_for_conv(models, conv_id);
|
||||
if (owner.has_value()) {
|
||||
httplib::Client cli(CHILD_ADDR, owner->port);
|
||||
@@ -1926,6 +1916,11 @@ void server_models_routes::init_routes() {
|
||||
cli.set_write_timeout(0, STREAM_LOOKUP_TIMEOUT_MS * 1000);
|
||||
auto resp = cli.Delete(child_path.c_str());
|
||||
(void) resp; // the child logs its own miss when the session is unknown there
|
||||
} else if (auto tracked = models.conv_models.lookup(conv_id); tracked.has_value()) {
|
||||
// the entry exists but its model is still loading: the forget below erases it,
|
||||
// which cancels the request parked in proxy_post before the generation starts
|
||||
SRV_INF("router stop for conv_id=%s while model name=%s is loading, cancelling the pending request\n",
|
||||
conv_id.c_str(), tracked->c_str());
|
||||
} else {
|
||||
SRV_WRN("router stop for unknown conv_id=%s, no owning child in the conv map\n",
|
||||
conv_id.c_str());
|
||||
|
||||
@@ -134,12 +134,24 @@ private:
|
||||
// proxy_request forwards a POST carrying an X-Conversation-Id. best effort: a stale entry just
|
||||
// makes the child answer not found and the client recovers. owns its lock, one mutex per struct
|
||||
struct conv_model_tracker {
|
||||
void remember(const std::string & conv_id, const std::string & model) {
|
||||
// returns the ticket of this registration, 0 when nothing was registered. erasing or
|
||||
// replacing the entry invalidates the ticket, which is how a stop cancels a request
|
||||
// parked in the model load wait
|
||||
uint64_t remember(const std::string & conv_id, const std::string & model) {
|
||||
if (conv_id.empty() || model.empty()) {
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
map[conv_id] = model;
|
||||
uint64_t ticket = next_ticket++;
|
||||
map[conv_id] = { model, ticket };
|
||||
return ticket;
|
||||
}
|
||||
|
||||
// false means a stop erased the entry or a newer request replaced it
|
||||
bool alive(const std::string & conv_id, uint64_t ticket) {
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
auto it = map.find(conv_id);
|
||||
return it != map.end() && it->second.ticket == ticket;
|
||||
}
|
||||
|
||||
std::optional<std::string> lookup(const std::string & conv_id) {
|
||||
@@ -151,7 +163,7 @@ private:
|
||||
if (it == map.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return it->second;
|
||||
return it->second.model;
|
||||
}
|
||||
|
||||
void forget(const std::string & conv_id) {
|
||||
@@ -163,8 +175,13 @@ private:
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex mu;
|
||||
std::unordered_map<std::string, std::string> map;
|
||||
struct entry_t {
|
||||
std::string model;
|
||||
uint64_t ticket;
|
||||
};
|
||||
std::mutex mu;
|
||||
uint64_t next_ticket = 1;
|
||||
std::unordered_map<std::string, entry_t> map;
|
||||
};
|
||||
|
||||
common_preset_context ctx_preset;
|
||||
@@ -249,7 +266,7 @@ public:
|
||||
bool ensure_model_ready(const std::string & name);
|
||||
|
||||
// proxy an HTTP request to the model instance
|
||||
server_http_res_ptr proxy_request(const server_http_req & req, const std::string & method, const std::string & name, bool update_last_used);
|
||||
server_http_res_ptr proxy_request(const server_http_req & req, const std::string & method, const std::string & name, bool update_last_used, bool detached = false);
|
||||
|
||||
// handle message sent from server_child::notify_to_router()
|
||||
// raw input must starts with CMD_CHILD_TO_ROUTER_STATE, followed by a JSON string
|
||||
|
||||
@@ -453,7 +453,7 @@ static server_http_res_ptr make_error_response(int status, const std::string & m
|
||||
|
||||
server_http_context::handler_t server_stream_make_get_handler() {
|
||||
return [](const server_http_req & req) -> server_http_res_ptr {
|
||||
// GET /v1/stream/<conv_id>?from=N replays buffered SSE bytes then blocks for live
|
||||
// GET /v1/stream?conv_id=<id>&from=N replays buffered SSE bytes then blocks for live
|
||||
// bytes until the session finalizes, streamed as text/event-stream for EventSource
|
||||
std::string conv_id = req.get_param("conv_id");
|
||||
if (conv_id.empty()) {
|
||||
@@ -560,13 +560,13 @@ server_http_context::handler_t server_stream_make_lookup_handler() {
|
||||
|
||||
server_http_context::handler_t server_stream_make_delete_handler() {
|
||||
return [](const server_http_req & req) -> server_http_res_ptr {
|
||||
// DELETE /v1/stream/<conv_id> is the explicit user Stop, cancels the producer and evicts
|
||||
// DELETE /v1/stream?conv_id=<id> is the explicit user Stop, cancels the producer and evicts
|
||||
// the buffer. idempotent, returns 204 even if the session was already gone
|
||||
std::string conv_id = req.get_param("conv_id");
|
||||
if (conv_id.empty()) {
|
||||
return make_error_response(400, "Missing conversation id in path", ERROR_TYPE_INVALID_REQUEST);
|
||||
}
|
||||
SRV_TRC("DELETE /v1/stream/%s -> evict_and_cancel\n", conv_id.c_str());
|
||||
SRV_TRC("DELETE /v1/stream conv_id=%s -> evict_and_cancel\n", conv_id.c_str());
|
||||
g_stream_sessions.evict_and_cancel(conv_id);
|
||||
auto res = std::make_unique<server_http_res>();
|
||||
res->status = 204;
|
||||
@@ -621,7 +621,7 @@ bool server_res_spipe::conn_alive() {
|
||||
|
||||
bool server_res_spipe::should_stop() {
|
||||
if (spipe) {
|
||||
// note: if DELETE /v1/stream/<conv_id> is called, is_cancelled() will be true
|
||||
// note: if DELETE /v1/stream is called for this conv, is_cancelled() will be true
|
||||
return spipe->is_cancelled();
|
||||
} else {
|
||||
return !conn_alive();
|
||||
|
||||
@@ -45,7 +45,13 @@ void server_stream_session_manager_start();
|
||||
void server_stream_session_manager_stop();
|
||||
|
||||
// route handler factories wired under /v1/stream/* by server.cpp
|
||||
// child-side handlers for the resumable stream routes. the conv id travels in the conv_id
|
||||
// query string because it can embed a model name containing slashes (org/repo), which the
|
||||
// decoded path would split before the param is captured
|
||||
server_http_context::handler_t server_stream_make_get_handler();
|
||||
// POST /v1/streams/lookup with body {"conversation_ids": [...]}: only answers for ids the
|
||||
// caller already owns (the WebUI passes the convs visible in its sidebar), the server never
|
||||
// lists ids it has not been asked about, so a random caller cannot enumerate live sessions
|
||||
server_http_context::handler_t server_stream_make_lookup_handler();
|
||||
server_http_context::handler_t server_stream_make_delete_handler();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "server-tools.h"
|
||||
|
||||
#include <sheredom/subprocess.h>
|
||||
#include "subproc.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
@@ -138,15 +138,14 @@ public:
|
||||
const std::function<bool(const std::string &)> & on_chunk = nullptr) const override {
|
||||
exec_result res;
|
||||
|
||||
subprocess_s proc;
|
||||
auto argv = to_cstr_vec(args);
|
||||
common_subproc proc;
|
||||
|
||||
int options = subprocess_option_no_window
|
||||
| subprocess_option_combined_stdout_stderr
|
||||
| subprocess_option_inherit_environment
|
||||
| subprocess_option_search_user_path;
|
||||
|
||||
if (subprocess_create(argv.data(), options, &proc) != 0) {
|
||||
if (!proc.create(args, options)) {
|
||||
res.output = "failed to spawn process";
|
||||
return res;
|
||||
}
|
||||
@@ -159,14 +158,14 @@ public:
|
||||
while (!done.load()) {
|
||||
if (std::chrono::steady_clock::now() >= deadline) {
|
||||
timed_out.store(true);
|
||||
subprocess_terminate(&proc);
|
||||
proc.terminate();
|
||||
return;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
});
|
||||
|
||||
FILE * f = subprocess_stdout(&proc);
|
||||
FILE * f = proc.stdout_file();
|
||||
std::string output;
|
||||
bool truncated = false;
|
||||
if (f) {
|
||||
@@ -177,7 +176,7 @@ public:
|
||||
if (output.size() + len <= max_output) {
|
||||
output.append(buf, len);
|
||||
if (on_chunk && !on_chunk(std::string(buf, len))) {
|
||||
subprocess_terminate(&proc);
|
||||
proc.terminate();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -195,8 +194,7 @@ public:
|
||||
timeout_thread.join();
|
||||
}
|
||||
|
||||
subprocess_join(&proc, &res.exit_code);
|
||||
subprocess_destroy(&proc);
|
||||
res.exit_code = proc.join();
|
||||
|
||||
res.output = output;
|
||||
res.timed_out = timed_out.load();
|
||||
@@ -207,16 +205,6 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
static std::vector<char *> to_cstr_vec(const std::vector<std::string> & v) {
|
||||
std::vector<char *> r;
|
||||
r.reserve(v.size() + 1);
|
||||
for (const auto & s : v) {
|
||||
r.push_back(const_cast<char *>(s.c_str()));
|
||||
}
|
||||
r.push_back(nullptr);
|
||||
return r;
|
||||
}
|
||||
|
||||
static const std::unordered_set<std::string> & junk_dir_names() {
|
||||
static const std::unordered_set<std::string> names = {
|
||||
".git", ".svn", ".hg", "node_modules", "__pycache__",
|
||||
@@ -1203,6 +1191,10 @@ static std::vector<std::unique_ptr<server_tool>> build_tools() {
|
||||
void server_tools::setup(const std::vector<std::string> & enabled_tools,
|
||||
server_mcp & mcp_mgr) {
|
||||
if (!enabled_tools.empty()) {
|
||||
if (!common_subproc::is_supported()) {
|
||||
throw std::runtime_error("subprocess is not enabled on this build");
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> enabled_set(enabled_tools.begin(), enabled_tools.end());
|
||||
auto all_tools = build_tools();
|
||||
|
||||
|
||||
@@ -272,10 +272,8 @@ int llama_server(common_params & params, int argc, char ** argv) {
|
||||
ctx_http.get ("/slots", ex_wrapper(routes.get_slots));
|
||||
ctx_http.post("/slots/:id_slot", ex_wrapper(routes.post_slots));
|
||||
|
||||
// resumable streaming, the conversation_id is the session identity end to end. router and
|
||||
// child wire different handlers under the same paths: a child binds the local session
|
||||
// factories, the router binds proxies that resolve the owning child through the
|
||||
// conv_id -> model map
|
||||
// resumable streaming: a child binds the local session factories, the router binds
|
||||
// proxies that resolve the owning child, see server-stream.h
|
||||
server_http_context::handler_t stream_get_h;
|
||||
server_http_context::handler_t streams_lookup_h;
|
||||
server_http_context::handler_t stream_delete_h;
|
||||
@@ -288,12 +286,9 @@ int llama_server(common_params & params, int argc, char ** argv) {
|
||||
streams_lookup_h = server_stream_make_lookup_handler();
|
||||
stream_delete_h = server_stream_make_delete_handler();
|
||||
}
|
||||
ctx_http.get ("/v1/stream/:conv_id", ex_wrapper(stream_get_h));
|
||||
// POST /v1/streams/lookup with body {"conversation_ids": [...]}. you can only ask for ids
|
||||
// you already own (the WebUI passes the convs visible in its sidebar). the server never
|
||||
// lists ids it has not been asked about, so a random caller cannot enumerate live sessions
|
||||
ctx_http.get ("/v1/stream", ex_wrapper(stream_get_h));
|
||||
ctx_http.post("/v1/streams/lookup", ex_wrapper(streams_lookup_h));
|
||||
ctx_http.del ("/v1/stream/:conv_id", ex_wrapper(stream_delete_h));
|
||||
ctx_http.del ("/v1/stream", ex_wrapper(stream_delete_h));
|
||||
|
||||
// Google Cloud Platform (Vertex AI) compat
|
||||
ctx_http.register_gcp_compat();
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import json
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from urllib.parse import quote
|
||||
import pytest
|
||||
from utils import *
|
||||
|
||||
server: ServerProcess
|
||||
|
||||
# a model name with slashes exercises the query string routing of the stream routes: the id
|
||||
# cannot travel as a path param because the decoded slash would split it before capture
|
||||
MODEL = "ggml-org/tinygemma3-GGUF:Q8_0"
|
||||
STREAM_ID = f"conv-stream-test::{MODEL}"
|
||||
QS = "conv_id=" + quote(STREAM_ID, safe="")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def create_server():
|
||||
global server
|
||||
server = ServerPreset.router()
|
||||
|
||||
|
||||
def test_stream_resume_and_stop_with_slashed_model_name():
|
||||
global server
|
||||
server.start()
|
||||
|
||||
content = ""
|
||||
for data in server.make_stream_request("POST", "/chat/completions", data={
|
||||
"model": MODEL,
|
||||
"stream": True,
|
||||
"max_tokens": 16,
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
}, headers={"X-Conversation-Id": STREAM_ID}):
|
||||
if data["choices"]:
|
||||
content += data["choices"][0]["delta"].get("content") or ""
|
||||
assert len(content) > 0
|
||||
|
||||
# the finished session replays from the beginning through the router
|
||||
res = server.make_request("GET", f"/v1/stream?{QS}&from=0")
|
||||
assert res.status_code == 200
|
||||
assert "data: " in str(res.body)
|
||||
|
||||
# the explicit stop reaches the owning child and evicts the session
|
||||
res = server.make_request("DELETE", f"/v1/stream?{QS}")
|
||||
assert res.status_code == 204
|
||||
res = server.make_request("GET", f"/v1/stream?{QS}&from=0")
|
||||
assert res.status_code == 404
|
||||
|
||||
|
||||
def test_stream_stop_during_model_load():
|
||||
global server
|
||||
server.start()
|
||||
|
||||
thread_error: list[ServerError] = []
|
||||
thread_done = threading.Event()
|
||||
|
||||
def fire_post():
|
||||
try:
|
||||
for _ in server.make_stream_request("POST", "/chat/completions", data={
|
||||
"model": MODEL,
|
||||
"stream": True,
|
||||
"max_tokens": 512,
|
||||
"messages": [{"role": "user", "content": "Count from 1 to 1000."}],
|
||||
}, headers={"X-Conversation-Id": STREAM_ID}):
|
||||
pass
|
||||
except ServerError as e:
|
||||
thread_error.append(e)
|
||||
finally:
|
||||
thread_done.set()
|
||||
|
||||
t = threading.Thread(target=fire_post)
|
||||
t.start()
|
||||
|
||||
# catch the autoload window, tiny models load fast so poll aggressively
|
||||
saw_loading = False
|
||||
deadline = time.time() + 5.0
|
||||
while time.time() < deadline and not thread_done.is_set():
|
||||
res = server.make_request("GET", "/models")
|
||||
status = next(m["status"]["value"] for m in res.body["data"] if m["id"] == MODEL)
|
||||
if status == "loading":
|
||||
saw_loading = True
|
||||
break
|
||||
time.sleep(0.002)
|
||||
if not saw_loading:
|
||||
t.join()
|
||||
pytest.skip("load window too short to be observed on this machine") # ty: ignore[too-many-positional-arguments]
|
||||
|
||||
# a stop during the load cancels the parked request instead of leaving an orphan
|
||||
res = server.make_request("DELETE", f"/v1/stream?{QS}")
|
||||
assert res.status_code == 204
|
||||
assert thread_done.wait(timeout=60)
|
||||
t.join()
|
||||
assert len(thread_error) == 1
|
||||
assert thread_error[0].code == 400
|
||||
assert "cancelled" in json.dumps(thread_error[0].body)
|
||||
res = server.make_request("GET", f"/v1/stream?{QS}&from=0")
|
||||
assert res.status_code == 404
|
||||
|
||||
|
||||
def test_stream_resumes_after_reload_during_model_load():
|
||||
global server
|
||||
server.start()
|
||||
|
||||
# raw socket client so the connection can be dropped mid load like a page reload
|
||||
body = json.dumps({
|
||||
"model": MODEL,
|
||||
"stream": True,
|
||||
"max_tokens": 16,
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
})
|
||||
request = (
|
||||
f"POST /v1/chat/completions HTTP/1.1\r\n"
|
||||
f"Host: {server.server_host}:{server.server_port}\r\n"
|
||||
f"Content-Type: application/json\r\n"
|
||||
f"X-Conversation-Id: {STREAM_ID}\r\n"
|
||||
f"Content-Length: {len(body)}\r\n"
|
||||
f"Connection: close\r\n\r\n{body}"
|
||||
)
|
||||
sock = socket.create_connection((server.server_host, server.server_port))
|
||||
sock.sendall(request.encode())
|
||||
|
||||
# drop the client while the model loads, poll aggressively to catch the window
|
||||
saw_loading = False
|
||||
saw_503 = False
|
||||
deadline = time.time() + 5.0
|
||||
while time.time() < deadline:
|
||||
res = server.make_request("GET", "/models")
|
||||
status = next(m["status"]["value"] for m in res.body["data"] if m["id"] == MODEL)
|
||||
if status == "loading":
|
||||
saw_loading = True
|
||||
break
|
||||
if status == "loaded":
|
||||
break
|
||||
time.sleep(0.002)
|
||||
sock.close()
|
||||
if not saw_loading:
|
||||
pytest.skip("load window too short to be observed on this machine") # ty: ignore[too-many-positional-arguments]
|
||||
|
||||
# while the model loads the resume route answers retry later, then the session appears,
|
||||
# receives the whole generation despite the dead client, and replays from the beginning
|
||||
deadline = time.time() + 60.0
|
||||
replay = None
|
||||
while time.time() < deadline:
|
||||
res = server.make_request("GET", f"/v1/stream?{QS}&from=0")
|
||||
if res.status_code == 503:
|
||||
saw_503 = True
|
||||
elif res.status_code == 200 and "data: " in str(res.body):
|
||||
replay = res
|
||||
break
|
||||
time.sleep(0.1)
|
||||
assert saw_503, "resume during the load did not answer 503"
|
||||
assert replay is not None, "session never became resumable after the client disconnect"
|
||||
+8
-1
@@ -13,6 +13,13 @@
|
||||
|
||||
const gauge = useContextGauge();
|
||||
|
||||
// The gauge hook wraps a processing state instance that only follows the
|
||||
// live stream while its own monitoring flag is set, so the card instance
|
||||
// starts monitoring like the dial does.
|
||||
$effect(() => {
|
||||
gauge.startMonitoring();
|
||||
});
|
||||
|
||||
let cardEl = $state<HTMLElement | null>(null);
|
||||
|
||||
// Any press outside the card and outside the dial closes the card.
|
||||
@@ -44,7 +51,7 @@
|
||||
<div
|
||||
role="status"
|
||||
bind:this={cardEl}
|
||||
class="absolute z-50 w-64 -translate-x-1/2 rounded-lg border border-border/50 bg-popover p-3 text-popover-foreground shadow-lg"
|
||||
class="absolute z-50 w-64 -translate-x-1/2 rounded-lg border border-border/50 bg-popover p-3 text-sm text-popover-foreground shadow-lg ring-1 ring-foreground/10"
|
||||
style="left: {gaugePopup.centerX}px; bottom: {gaugePopup.bottom}px"
|
||||
onpointerenter={gaugeCardEnter}
|
||||
onpointerleave={gaugeCardLeave}
|
||||
|
||||
+6
-3
@@ -10,7 +10,7 @@
|
||||
} from '$lib/components/app';
|
||||
import { getMessageEditContext } from '$lib/contexts';
|
||||
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
||||
import { isLoading, isChatStreaming } from '$lib/stores/chat.svelte';
|
||||
import { chatStore, isLoading, isChatStreaming } from '$lib/stores/chat.svelte';
|
||||
import { modelLoadProgressText } from '$lib/utils';
|
||||
import { MessageRole } from '$lib/enums';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
@@ -82,8 +82,11 @@
|
||||
let hasNoContent = $derived(!message?.content?.trim());
|
||||
let isActivelyProcessing = $derived(isCurrentlyLoading || isStreaming);
|
||||
|
||||
// during a router auto-load the message has no model yet, so target the selected one
|
||||
let loadTargetModel = $derived(message.model ?? modelsStore.selectedModelName);
|
||||
// during a router auto-load the message has no model yet: target the model frozen in the
|
||||
// persisted stream state (survives a reload), then fall back to the dropdown selection
|
||||
let loadTargetModel = $derived(
|
||||
message.model ?? chatStore.getResumeModel(message.convId) ?? modelsStore.selectedModelName
|
||||
);
|
||||
let modelLoadProgress = $derived(
|
||||
isRouter && loadTargetModel ? modelsStore.getLoadProgress(loadTargetModel) : null
|
||||
);
|
||||
|
||||
+11
-2
@@ -7,7 +7,7 @@
|
||||
import { getMessageEditContext } from '$lib/contexts';
|
||||
import { KeyboardKey, MessageRole } from '$lib/enums';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { isIMEComposing } from '$lib/utils';
|
||||
import { autoResizeTextarea, isIMEComposing } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
@@ -91,6 +91,11 @@
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
});
|
||||
$effect(() => {
|
||||
if (editCtx.isEditing && textareaElement) {
|
||||
autoResizeTextarea(textareaElement);
|
||||
}
|
||||
});
|
||||
|
||||
function toggleExpand() {
|
||||
isExpanded = !isExpanded;
|
||||
@@ -105,11 +110,15 @@
|
||||
{#if editCtx.isEditing}
|
||||
<div class="w-full max-w-[80%]">
|
||||
<textarea
|
||||
style="max-height: var(--max-message-height);"
|
||||
bind:this={textareaElement}
|
||||
value={editCtx.editedContent}
|
||||
class="min-h-[60px] w-full resize-none rounded-2xl px-3 py-2 text-sm {INPUT_CLASSES}"
|
||||
onkeydown={handleEditKeydown}
|
||||
oninput={(e) => editCtx.setContent(e.currentTarget.value)}
|
||||
oninput={(e) => {
|
||||
autoResizeTextarea(e.currentTarget);
|
||||
editCtx.setContent(e.currentTarget.value);
|
||||
}}
|
||||
placeholder="Edit system message..."
|
||||
></textarea>
|
||||
|
||||
|
||||
@@ -31,11 +31,11 @@
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { serverLoading, serverError } from '$lib/stores/server.svelte';
|
||||
import { parseFilesToMessageExtras } from '$lib/utils/browser-only';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { onDestroy, onMount, tick } from 'svelte';
|
||||
import ChatScreenGreeting from './ChatScreenGreeting.svelte';
|
||||
import ChatScreenActionScrollDown from './ChatScreenActionScrollDown.svelte';
|
||||
import ChatScreenDialogsAndAlerts from './ChatScreenDialogsAndAlerts.svelte';
|
||||
import { ROUTES } from '$lib/constants';
|
||||
import { LANDING_SETTLE_MAX_MS, LANDING_STABLE_FRAMES, ROUTES } from '$lib/constants';
|
||||
|
||||
let { showCenteredEmpty = false } = $props();
|
||||
|
||||
@@ -128,6 +128,41 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
let lastScrolledConversationId: string | null = null;
|
||||
|
||||
// Lands at the bottom of a conversation the first time its messages
|
||||
// render, whether the route comes from another conversation or from a
|
||||
// non-conversation route. The page keeps growing after the first pin
|
||||
// without DOM mutations (content-visibility size realizations, syntax
|
||||
// highlight passes), so the instant pin repeats every frame until the
|
||||
// height settles, bailing out on user scroll or conversation change.
|
||||
async function handleMessagesReady(messageCount: number) {
|
||||
if (messageCount === 0) return;
|
||||
const id = activeConversation()?.id ?? null;
|
||||
if (!id || id === lastScrolledConversationId) return;
|
||||
lastScrolledConversationId = id;
|
||||
await tick();
|
||||
autoScroll.scrollToBottom();
|
||||
|
||||
const container = scroll.chatScrollContainer;
|
||||
if (!container) return;
|
||||
const started = performance.now();
|
||||
let stableFrames = 0;
|
||||
let lastHeight = container.scrollHeight;
|
||||
const settle = () => {
|
||||
if (autoScroll.userScrolledUp) return;
|
||||
if (activeConversation()?.id !== id) return;
|
||||
autoScroll.scrollToBottom();
|
||||
const height = container.scrollHeight;
|
||||
stableFrames = height === lastHeight ? stableFrames + 1 : 0;
|
||||
lastHeight = height;
|
||||
if (stableFrames >= LANDING_STABLE_FRAMES) return;
|
||||
if (performance.now() - started > LANDING_SETTLE_MAX_MS) return;
|
||||
requestAnimationFrame(settle);
|
||||
};
|
||||
requestAnimationFrame(settle);
|
||||
}
|
||||
|
||||
function handleSendLikeScroll() {
|
||||
if (!isMobile.current) {
|
||||
autoScroll.enable();
|
||||
@@ -246,6 +281,7 @@
|
||||
{#if !isEmpty}
|
||||
<ChatMessages
|
||||
messages={activeMessages()}
|
||||
onMessagesReady={handleMessagesReady}
|
||||
onUserAction={() => {
|
||||
handleSendLikeScroll();
|
||||
}}
|
||||
|
||||
+13
-3
@@ -159,8 +159,10 @@
|
||||
try {
|
||||
const input = document.createElement('input');
|
||||
|
||||
// No `accept` filter: iOS resolves each entry to a UTI and has none for
|
||||
// `.jsonl`, which greys out exported conversations in the file picker.
|
||||
// `parseImportFile` detects the format from the file contents instead.
|
||||
input.type = HtmlInputType.FILE;
|
||||
input.accept = `${FileExtensionText.JSON},${FileExtensionText.JSONL},${FileExtensionText.ZIP}`;
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement)?.files?.[0];
|
||||
@@ -199,9 +201,17 @@
|
||||
.snapshot(fullImportData)
|
||||
.filter((item) => selectedIds.has(item.conv.id));
|
||||
|
||||
await conversationsStore.importConversationsData(selectedData);
|
||||
const { imported, skipped } = await conversationsStore.importConversationsData(selectedData);
|
||||
|
||||
importedConversations = selectedConversations;
|
||||
// A conversation already in the database is left untouched, so the summary
|
||||
// lists what was written and the toast accounts for the rest.
|
||||
if (skipped.length > 0) {
|
||||
toast.info(
|
||||
`Skipped ${skipped.length} conversation${skipped.length === 1 ? '' : 's'} already in your library`
|
||||
);
|
||||
}
|
||||
|
||||
importedConversations = imported;
|
||||
showImportSummary = true;
|
||||
showExportSummary = false;
|
||||
showImportDialog = false;
|
||||
|
||||
@@ -21,7 +21,11 @@ export const API_TOOLS = {
|
||||
EXECUTE: '/tools'
|
||||
};
|
||||
|
||||
// resumable stream routes, the conv::model identity is appended as a path segment
|
||||
// resumable stream routes, the conv::model identity travels as the conv_id query param
|
||||
// because model names can contain slashes that a path segment cannot carry
|
||||
// resume retry cadence while the owning model is still loading (server answers 503)
|
||||
export const STREAM_RESUME_RETRY_MS = 2000;
|
||||
|
||||
export const API_STREAM = {
|
||||
BASE: './v1/stream',
|
||||
LOOKUP: './v1/streams/lookup'
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
export const AUTO_SCROLL_INTERVAL = 100;
|
||||
// Conversation landing: the page keeps growing after the first bottom pin
|
||||
// without DOM mutations (content-visibility size realizations, syntax
|
||||
// highlight passes), so the pin repeats every frame until the height holds
|
||||
// for this many consecutive frames, bounded by the time cap below.
|
||||
export const LANDING_STABLE_FRAMES = 10;
|
||||
export const LANDING_SETTLE_MAX_MS = 1000;
|
||||
// Chat main view: tight threshold because scroll-here events come from
|
||||
// discrete assistant-message appends.
|
||||
export const AUTO_SCROLL_AT_BOTTOM_THRESHOLD = 10;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// First bytes of every ZIP local file header ("PK"). Import detects an archive
|
||||
// from these bytes rather than from the filename, which the OS may not preserve.
|
||||
export const ZIP_MAGIC = [0x50, 0x4b];
|
||||
@@ -13,6 +13,7 @@ export * from './storage';
|
||||
export * from './attachment-menu';
|
||||
export * from './auto-scroll';
|
||||
export * from './context-gauge-popup';
|
||||
export * from './conversation-import';
|
||||
export * from './binary-detection';
|
||||
export * from './built-in-tools';
|
||||
export * from './cache';
|
||||
|
||||
@@ -7,6 +7,9 @@ export const EXPORT_CONV_NAME_SUFFIX_MAX_LENGTH = 20;
|
||||
// Characters to keep in the ISO timestamp. 19 keeps 2026-01-01T00:00:00
|
||||
export const ISO_TIMESTAMP_SLICE_LENGTH = 19;
|
||||
|
||||
// Producer marker carried by the session record of a JSONL export
|
||||
export const SESSION_HARNESS = 'llama.app';
|
||||
|
||||
// Replacements for making the conversation title filename-friendly
|
||||
export const NON_ALPHANUMERIC_REGEX = /[^a-z0-9]/gi;
|
||||
export const EXPORT_CONV_NONALNUM_REPLACEMENT = '_';
|
||||
|
||||
@@ -14,12 +14,15 @@ export const SANDBOX_EMPTY_OUTPUT = '(no output)';
|
||||
export const SANDBOX_TRUNCATION_NOTICE = '[output truncated]';
|
||||
|
||||
const NERDAMER_DESCRIPTION = `
|
||||
Symbolic/numeric math via \`nerdamer\` (pre-loaded, do not require, use it directly).
|
||||
nerdamer('diff(sin(x)/x,x)') or nerdamer.diff('sin(x)/x','x') → Expression; convert with .toString()/.text()/.toTeX(), or .evaluate() (→ still Expression, then .toString()).
|
||||
nerdamer(expr,{x:2}) substitutes only; chain .evaluate() or pass 'numer' for numeric result.
|
||||
solve(expr,var)→Symbol[]; solveEquations([eq1,..])→[[var,val],..] pairs.
|
||||
Functions: simplify/expand/factor(expr), diff(expr,var[,n]), integrate(expr,var), defint(expr,from,to,var), limit(expr,var,to), laplace(expr,t,s), ilt(expr,s,t), gcd/lcm(a,b), roots/coeffs/partfrac(expr,var), pfactor(n), numer/decimals/erf(expr), product/sum(expr,var,from,to), mean/median/stdev/variance(...vals).
|
||||
Object.keys(nerdamer).filter(k=>typeof nerdamer[k]==='function') lists all available functions. If you need a function not documented above, list them first — do not guess function names.`;
|
||||
Symbolic/numeric math via \`nerdamer\`
|
||||
nerdamer(expr,subs?,opts?)/nerdamer.func(...)→Expression Format via .text(fmt?) (fmt: 'decimals'|'fractions'|'scientific') eval via .evaluate(subs?)
|
||||
nerdamer(expr,{x:2}) substitutes numeric via opts 'numer' or .evaluate()
|
||||
simplify/expand/factor(expr) div/gcd/lcm(...) coeffs/partfrac(expr,var)
|
||||
diff/integrate(expr,var) defint(expr,lo,hi,var?) sum/product(expr,var,lo,hi) limit(expr,var,pt)
|
||||
solve(expr,var) solveEquations([eq1,eq2],[var1,var2])
|
||||
polarform/rectform/arg/realpart/imagpart(z)
|
||||
set/get Var/Constant(name,val?) setFunction(name,[params],body)
|
||||
IMPORTANT:Identifier 'nerdamer' has already been declared, use it directly`;
|
||||
|
||||
/**
|
||||
* Build the sandbox tool definition. When `includeSymbolicMath` is true,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Discriminator of a record line in the JSONL conversation format. A session
|
||||
* record opens a conversation and carries its properties; every following
|
||||
* message record belongs to it.
|
||||
*/
|
||||
export enum SessionRecordType {
|
||||
SESSION = 'session',
|
||||
MESSAGE = 'message'
|
||||
}
|
||||
@@ -27,6 +27,8 @@ export {
|
||||
ReasoningFormat
|
||||
} from './chat.enums';
|
||||
|
||||
export { SessionRecordType } from './conversation-import.enums';
|
||||
|
||||
export { ReasoningEffort } from './reasoning-effort.enums';
|
||||
|
||||
export {
|
||||
|
||||
@@ -343,6 +343,9 @@ export class ChatService {
|
||||
// model the ::model suffix keeps the per model session distinct
|
||||
if (stream && conversationId) {
|
||||
headers['X-Conversation-Id'] = streamIdentity(conversationId, options.model);
|
||||
// persist the pending stream before the fetch: a reload during the model load or
|
||||
// the prompt processing must still find its way back to the session once it exists
|
||||
ChatService.saveStreamState(conversationId, 0, options.model ?? null);
|
||||
}
|
||||
|
||||
const response = await fetch(API_CHAT.COMPLETIONS, {
|
||||
@@ -353,6 +356,11 @@ export class ChatService {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// a rejected request (including one cancelled by a stop during the model load)
|
||||
// leaves nothing to resume
|
||||
if (conversationId) {
|
||||
ChatService.clearStreamState(conversationId);
|
||||
}
|
||||
const error = await ChatService.parseErrorResponse(response);
|
||||
|
||||
if (onError) {
|
||||
@@ -512,7 +520,7 @@ export class ChatService {
|
||||
if (!conversationId) return;
|
||||
try {
|
||||
const id = streamIdentity(conversationId, model);
|
||||
await fetch(`${API_STREAM.BASE}/${encodeURIComponent(id)}`, {
|
||||
await fetch(`${API_STREAM.BASE}?conv_id=${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
@@ -605,6 +613,26 @@ export class ChatService {
|
||||
* existing SSE parser drains it like a fresh stream. The server returns 200 on success, 404 if
|
||||
* no session exists for the conv_id, and 400 if the offset is below the dropped prefix.
|
||||
*/
|
||||
// probe the resume route status without consuming the stream: the SSE route has no HEAD,
|
||||
// so issue the GET and abort it right after the status line. 0 on network error
|
||||
static async probeResumeStatus(streamId: string): Promise<number> {
|
||||
if (!streamId) return 0;
|
||||
const ac = new AbortController();
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`${API_STREAM.BASE}?conv_id=${encodeURIComponent(streamId)}&from=0`,
|
||||
{
|
||||
headers: getAuthHeaders(),
|
||||
signal: ac.signal
|
||||
}
|
||||
);
|
||||
ac.abort();
|
||||
return resp.status;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static async resumeStream(
|
||||
conversationId: string,
|
||||
signal?: AbortSignal,
|
||||
@@ -614,7 +642,7 @@ export class ChatService {
|
||||
const state = ChatService.getStreamState(conversationId);
|
||||
const from = state?.bytesReceived ?? 0;
|
||||
const id = streamIdentity(conversationId, model);
|
||||
const url = `${API_STREAM.BASE}/${encodeURIComponent(id)}?from=${from}`;
|
||||
const url = `${API_STREAM.BASE}?conv_id=${encodeURIComponent(id)}&from=${from}`;
|
||||
return await fetch(url, { method: 'GET', signal, headers: getAuthHeaders() });
|
||||
}
|
||||
|
||||
|
||||
@@ -554,12 +554,13 @@ export class DatabaseService {
|
||||
* Skips conversations that already exist.
|
||||
*
|
||||
* @param data - Array of { conv, messages } objects
|
||||
* @returns The conversations written to the database and the ones skipped
|
||||
*/
|
||||
static async importConversations(
|
||||
data: { conv: DatabaseConversation; messages: DatabaseMessage[] }[]
|
||||
): Promise<{ imported: number; skipped: number }> {
|
||||
let importedCount = 0;
|
||||
let skippedCount = 0;
|
||||
): Promise<{ imported: DatabaseConversation[]; skipped: DatabaseConversation[] }> {
|
||||
const imported: DatabaseConversation[] = [];
|
||||
const skipped: DatabaseConversation[] = [];
|
||||
|
||||
return await db.transaction(
|
||||
'rw',
|
||||
@@ -570,8 +571,7 @@ export class DatabaseService {
|
||||
|
||||
const existing = await db[IDXDB_TABLES.conversations].get(conv.id);
|
||||
if (existing) {
|
||||
console.warn(`Conversation "${conv.name}" already exists, skipping...`);
|
||||
skippedCount++;
|
||||
skipped.push(conv);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -580,10 +580,10 @@ export class DatabaseService {
|
||||
await db[IDXDB_TABLES.messages].put(msg);
|
||||
}
|
||||
|
||||
importedCount++;
|
||||
imported.push(conv);
|
||||
}
|
||||
|
||||
return { imported: importedCount, skipped: skippedCount };
|
||||
return { imported, skipped };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||
import { DatabaseService } from '$lib/services/database.service';
|
||||
import { ChatService } from '$lib/services/chat.service';
|
||||
import { STREAM_RESUME_RETRY_MS } from '$lib/constants/api-endpoints';
|
||||
import { streamIdentity } from '$lib/utils/stream-identity';
|
||||
import { getAuthHeaders } from '$lib/utils/api-headers';
|
||||
import { CONTENT_TYPE_HEADER } from '$lib/constants';
|
||||
@@ -78,7 +79,7 @@ class ChatStore {
|
||||
// true while the active conversation streams reasoning content but no visible content yet
|
||||
isReasoning = $state(false);
|
||||
// resumable stream connection state for the active conversation
|
||||
// streaming -> bytes flowing normally, resuming -> waiting on /v1/stream/:id reconnect, lost -> unrecoverable
|
||||
// streaming -> bytes flowing normally, resuming -> waiting on /v1/stream reconnect, lost -> unrecoverable
|
||||
streamConnectionState = $state<StreamConnectionState>(StreamConnectionState.STREAMING);
|
||||
chatLoadingStates = new SvelteMap<string, boolean>();
|
||||
chatReasoningStates = new SvelteMap<string, boolean>();
|
||||
@@ -94,6 +95,11 @@ class ChatStore {
|
||||
// off when one conv finishes while another is still streaming. mirrors chatLoadingStates
|
||||
// in scope but tracks the attach + tee replay path specifically
|
||||
private attachingConvs = new SvelteSet<string>();
|
||||
// pending resume retry timers while an owning model loads, one per conv
|
||||
private resumeRetryTimers = new SvelteMap<string, ReturnType<typeof setTimeout>>();
|
||||
// convs whose resume waits on a model load: their loading state belongs to the retry loop,
|
||||
// so discoverActiveStream must not treat it as a live send and bail
|
||||
private resumePendingConvs = new SvelteSet<string>();
|
||||
// in-flight discoverActiveStream guard, keyed by conv id
|
||||
private discoveringConvs = new SvelteSet<string>();
|
||||
private abortControllers = new SvelteMap<string, AbortController>();
|
||||
@@ -263,7 +269,7 @@ class ChatStore {
|
||||
const id = streamId || streamIdentity(convId, selectedModelName());
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`./v1/stream/${encodeURIComponent(id)}?from=0`, {
|
||||
response = await fetch(`./v1/stream?conv_id=${encodeURIComponent(id)}&from=0`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -438,13 +444,22 @@ class ChatStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Model frozen at send time for a stream awaiting resume, from the persisted stream state.
|
||||
* The load progress indicator targets it after a reload, when the message row has no model
|
||||
* yet and the dropdown selection may not be restored.
|
||||
*/
|
||||
getResumeModel(convId: string): string | null {
|
||||
return ChatService.getStreamState(convId)?.model ?? null;
|
||||
}
|
||||
|
||||
async discoverActiveStream(convId: string): Promise<void> {
|
||||
if (!convId) return;
|
||||
if (this.chatStreamingStates.has(convId)) return;
|
||||
if (this.chatLoadingStates.get(convId)) return;
|
||||
if (this.chatLoadingStates.get(convId) && !this.resumePendingConvs.has(convId)) return;
|
||||
// concurrency guard: another discover may already be running for this conv (typical race
|
||||
// between mount and visibilitychange on tab switch). a second concurrent fetch on the same
|
||||
// /v1/stream/<id> would duplicate every byte into the DB message, this guard bounces it
|
||||
// /v1/stream would duplicate every byte into the DB message, this guard bounces it
|
||||
if (this.discoveringConvs.has(convId)) return;
|
||||
this.discoveringConvs.add(convId);
|
||||
|
||||
@@ -470,6 +485,38 @@ class ChatStore {
|
||||
if (!localState) {
|
||||
return;
|
||||
}
|
||||
// quiet status probe first: a full attach flips the loading UI on every try, probing
|
||||
// keeps the retry loop invisible while the owning model is still loading (503)
|
||||
const status = await ChatService.probeResumeStatus(streamId);
|
||||
if (status === 503) {
|
||||
// make the wait visible: the empty assistant row persisted at send time renders
|
||||
// the processing info, whose model load percentage flows from the models feed
|
||||
this.resumePendingConvs.add(convId);
|
||||
this.setChatLoading(convId, true);
|
||||
if (!this.resumeRetryTimers.has(convId)) {
|
||||
this.resumeRetryTimers.set(
|
||||
convId,
|
||||
setTimeout(() => {
|
||||
this.resumeRetryTimers.delete(convId);
|
||||
void this.discoverActiveStream(convId);
|
||||
}, STREAM_RESUME_RETRY_MS)
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.resumePendingConvs.delete(convId) && status !== 200) {
|
||||
// the wait is over without a session to attach, drop the visible loading state
|
||||
this.setChatLoading(convId, false);
|
||||
}
|
||||
if (status === 0) {
|
||||
// transient network failure, the next mount or visibility change retries
|
||||
return;
|
||||
}
|
||||
if (status !== 200) {
|
||||
// the session is gone (stopped, TTL expired), nothing to resume anymore
|
||||
ChatService.clearStreamState(convId);
|
||||
return;
|
||||
}
|
||||
await this.attachServerStream(convId, streamId);
|
||||
// if attachServerStream failed (session gone, TTL expired), clear the local state to avoid retrying forever
|
||||
if (!this.chatStreamingStates.has(convId) && !this.chatLoadingStates.get(convId)) {
|
||||
@@ -1469,8 +1516,16 @@ class ChatStore {
|
||||
// detached drain keeps producing tokens until eos or max_tokens. use the frozen identity
|
||||
// captured when the session started, not the live dropdown
|
||||
const streamStateForStop = this.chatStreamingStates.get(convId);
|
||||
const modelForStop = streamStateForStop?.model;
|
||||
const modelForStop = streamStateForStop?.model ?? ChatService.getStreamState(convId)?.model;
|
||||
void ChatService.cancelServerStream(convId, modelForStop);
|
||||
// an explicit stop leaves nothing to resume and kills a pending resume retry
|
||||
ChatService.clearStreamState(convId);
|
||||
const retryTimer = this.resumeRetryTimers.get(convId);
|
||||
if (retryTimer !== undefined) {
|
||||
clearTimeout(retryTimer);
|
||||
this.resumeRetryTimers.delete(convId);
|
||||
}
|
||||
this.resumePendingConvs.delete(convId);
|
||||
this.abortRequest(convId);
|
||||
this.setChatLoading(convId, false);
|
||||
this.clearChatStreaming(convId);
|
||||
|
||||
@@ -30,11 +30,11 @@ import type { McpServerOverride } from '$lib/types/database';
|
||||
import { zipSync, unzipSync, strToU8, strFromU8 } from 'fflate';
|
||||
import {
|
||||
MessageRole,
|
||||
HtmlInputType,
|
||||
FileExtensionText,
|
||||
MimeTypeText,
|
||||
MimeTypeApplication,
|
||||
ReasoningEffort
|
||||
ReasoningEffort,
|
||||
SessionRecordType
|
||||
} from '$lib/enums';
|
||||
import {
|
||||
ISO_DATE_TIME_SEPARATOR,
|
||||
@@ -47,7 +47,10 @@ import {
|
||||
ISO_TIME_SEPARATOR_REPLACEMENT,
|
||||
NON_ALPHANUMERIC_REGEX,
|
||||
MULTIPLE_UNDERSCORE_REGEX,
|
||||
REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY
|
||||
REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY,
|
||||
NEWLINE,
|
||||
SESSION_HARNESS,
|
||||
ZIP_MAGIC
|
||||
} from '$lib/constants';
|
||||
|
||||
import { ROUTES } from '$lib/constants/routes';
|
||||
@@ -914,30 +917,35 @@ class ConversationsStore {
|
||||
|
||||
/**
|
||||
* Serializes a session (a conversation with its messages) as JSONL.
|
||||
* The first line is the session header (a `type: 'session'` record carrying the
|
||||
* conversation properties); each subsequent line is a single message.
|
||||
* The first line is the session header (a `SessionRecordType.SESSION` record
|
||||
* carrying the conversation properties); each subsequent line is a single message.
|
||||
* @param data - The exported conversation payload
|
||||
* @returns The JSONL string (one record per line)
|
||||
*/
|
||||
serializeSessionToJsonl(data: ExportedConversation): string {
|
||||
const { conv, messages } = data;
|
||||
|
||||
const sessionLine = JSON.stringify({ type: 'session', harness: 'llama.app', ...conv });
|
||||
const sessionLine = JSON.stringify({
|
||||
type: SessionRecordType.SESSION,
|
||||
harness: SESSION_HARNESS,
|
||||
...conv
|
||||
});
|
||||
const messageLines = messages.map((message: DatabaseMessage) => {
|
||||
// `toolCalls` is stored as a JSON string; drop it when empty, otherwise parse it.
|
||||
const { toolCalls, ...rest } = message;
|
||||
const normalized = toolCalls ? { ...rest, toolCalls: JSON.parse(toolCalls) } : rest;
|
||||
|
||||
return JSON.stringify({ type: 'message', message: normalized });
|
||||
return JSON.stringify({ type: SessionRecordType.MESSAGE, message: normalized });
|
||||
});
|
||||
|
||||
return [sessionLine, ...messageLines].join('\n');
|
||||
return [sessionLine, ...messageLines].join(NEWLINE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the JSONL session format produced by {@link serializeSessionToJsonl}.
|
||||
* A `type: 'session'` line starts a new session; following `type: 'message'`
|
||||
* lines are appended to it. Supports multiple sessions in a single file.
|
||||
* A `SessionRecordType.SESSION` line starts a new session; following
|
||||
* `SessionRecordType.MESSAGE` lines are appended to it. Supports multiple
|
||||
* sessions in a single file.
|
||||
* @param text - The JSONL file contents
|
||||
* @returns The parsed conversations with their messages
|
||||
*/
|
||||
@@ -945,20 +953,20 @@ class ConversationsStore {
|
||||
const sessions: ExportedConversation[] = [];
|
||||
let current: ExportedConversation | null = null;
|
||||
|
||||
for (const line of text.split('\n')) {
|
||||
for (const line of text.split(NEWLINE)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
const record = JSON.parse(trimmed);
|
||||
|
||||
if (record.type === 'session') {
|
||||
if (record.type === SessionRecordType.SESSION) {
|
||||
// Drop the discriminator and harness marker; the rest is the conversation.
|
||||
const conv = { ...record };
|
||||
delete conv.type;
|
||||
delete conv.harness;
|
||||
current = { conv: conv as DatabaseConversation, messages: [] };
|
||||
sessions.push(current);
|
||||
} else if (record.type === 'message') {
|
||||
} else if (record.type === SessionRecordType.MESSAGE) {
|
||||
if (!current) {
|
||||
throw new Error('Invalid JSONL: message record before any session record');
|
||||
}
|
||||
@@ -977,27 +985,47 @@ class ConversationsStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an import file into conversations, accepting the current `.jsonl` and
|
||||
* `.zip` formats as well as the legacy `.json` format.
|
||||
* Reports whether the text is the JSONL session format, whose first non-empty
|
||||
* line is a `SessionRecordType.SESSION` record. A legacy JSON export starts
|
||||
* with an array or an object that has no such discriminator.
|
||||
* @param text - The file contents
|
||||
*/
|
||||
private isSessionsJsonl(text: string): boolean {
|
||||
const trimmed = text.trimStart();
|
||||
const lineEnd = trimmed.indexOf(NEWLINE);
|
||||
const firstLine = lineEnd === -1 ? trimmed : trimmed.slice(0, lineEnd);
|
||||
|
||||
try {
|
||||
return JSON.parse(firstLine).type === SessionRecordType.SESSION;
|
||||
} catch {
|
||||
// Not a standalone JSON record, so not the JSONL format.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an import file into conversations, accepting the current JSONL and
|
||||
* ZIP formats as well as the legacy JSON format. The format comes from the
|
||||
* contents, so an import works whatever the file is named.
|
||||
* @param file - The user-selected file
|
||||
* @returns The parsed conversations with their messages
|
||||
*/
|
||||
async parseImportFile(file: File): Promise<ExportedConversation[]> {
|
||||
const name = file.name.toLowerCase();
|
||||
const bytes = new Uint8Array(await file.arrayBuffer());
|
||||
|
||||
if (name.endsWith(FileExtensionText.ZIP)) {
|
||||
const entries = unzipSync(new Uint8Array(await file.arrayBuffer()));
|
||||
if (ZIP_MAGIC.every((byte, index) => bytes[index] === byte)) {
|
||||
const entries = unzipSync(bytes);
|
||||
const sessions: ExportedConversation[] = [];
|
||||
for (const [entryName, bytes] of Object.entries(entries)) {
|
||||
for (const [entryName, entryBytes] of Object.entries(entries)) {
|
||||
if (!entryName.toLowerCase().endsWith(FileExtensionText.JSONL)) continue;
|
||||
sessions.push(...this.parseSessionsJsonl(strFromU8(bytes)));
|
||||
sessions.push(...this.parseSessionsJsonl(strFromU8(entryBytes)));
|
||||
}
|
||||
return sessions;
|
||||
}
|
||||
|
||||
const text = await file.text();
|
||||
const text = strFromU8(bytes);
|
||||
|
||||
if (name.endsWith(FileExtensionText.JSONL)) {
|
||||
if (this.isSessionsJsonl(text)) {
|
||||
return this.parseSessionsJsonl(text);
|
||||
}
|
||||
|
||||
@@ -1103,73 +1131,14 @@ class ConversationsStore {
|
||||
this.downloadConversationFile({ conv: conversation, messages });
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports conversations from a JSON file
|
||||
* Opens file picker and processes the selected file
|
||||
* @returns The list of imported conversations
|
||||
*/
|
||||
async importConversations(): Promise<DatabaseConversation[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const input = document.createElement('input');
|
||||
input.type = HtmlInputType.FILE;
|
||||
input.accept = FileExtensionText.JSON;
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement)?.files?.[0];
|
||||
|
||||
if (!file) {
|
||||
reject(new Error('No file selected'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
const parsedData = JSON.parse(text);
|
||||
let importedData: ExportedConversations;
|
||||
|
||||
if (Array.isArray(parsedData)) {
|
||||
importedData = parsedData;
|
||||
} else if (
|
||||
parsedData &&
|
||||
typeof parsedData === 'object' &&
|
||||
'conv' in parsedData &&
|
||||
'messages' in parsedData
|
||||
) {
|
||||
importedData = [parsedData];
|
||||
} else {
|
||||
throw new Error('Invalid file format');
|
||||
}
|
||||
|
||||
const result = await DatabaseService.importConversations(importedData);
|
||||
toast.success(`Imported ${result.imported} conversation(s), skipped ${result.skipped}`);
|
||||
|
||||
await this.loadConversations();
|
||||
|
||||
const importedConversations = (
|
||||
Array.isArray(importedData) ? importedData : [importedData]
|
||||
).map((item) => item.conv);
|
||||
|
||||
resolve(importedConversations);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.error('Failed to import conversations:', err);
|
||||
toast.error('Import failed', { description: message });
|
||||
reject(new Error(`Import failed: ${message}`));
|
||||
}
|
||||
};
|
||||
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports conversations from provided data (without file picker)
|
||||
* @param data - Array of conversation data with messages
|
||||
* @returns Import result with counts
|
||||
* @returns The conversations written to the database and the ones skipped
|
||||
*/
|
||||
async importConversationsData(
|
||||
data: ExportedConversations
|
||||
): Promise<{ imported: number; skipped: number }> {
|
||||
): Promise<{ imported: DatabaseConversation[]; skipped: DatabaseConversation[] }> {
|
||||
const result = await DatabaseService.importConversations(data);
|
||||
await this.loadConversations();
|
||||
return result;
|
||||
|
||||
@@ -161,9 +161,3 @@ export function generateModalityErrorMessage(
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate file input accept string based on model modalities
|
||||
* @param capabilities - The modality capabilities to check against
|
||||
* @returns Accept string for HTML file input element
|
||||
*/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user