mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-10-20 19:57:28 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
339f3242c7 | ||
|
|
9a3b91c680 | ||
|
|
5c0b49a23b | ||
|
|
1242be66f6 | ||
|
|
84e66d0321 | ||
|
|
52ffa02745 | ||
|
|
839a572a4c | ||
|
|
ead9f32058 | ||
|
|
b684b498b6 | ||
|
|
4a45de4f2c | ||
|
|
7e3eeee048 | ||
|
|
5ba3b9fd3d | ||
|
|
5a9a34d888 | ||
|
|
39da130fcd | ||
|
|
4e201388c3 | ||
|
|
1ba568c5ad | ||
|
|
36140dbee2 | ||
|
|
df11702136 | ||
|
|
9df0b73c70 | ||
|
|
b38960f520 | ||
|
|
a01ce9ffc6 | ||
|
|
8115b4c31f | ||
|
|
119f68d7d4 |
@@ -7,15 +7,7 @@ on:
|
||||
- master
|
||||
paths: [
|
||||
'.github/workflows/build-webgpu.yml',
|
||||
'**/CMakeLists.txt',
|
||||
'**/.cmake',
|
||||
'**/*.h',
|
||||
'**/*.hpp',
|
||||
'**/*.c',
|
||||
'**/*.cpp',
|
||||
'**/*.wgsl',
|
||||
'**/*.tmpl',
|
||||
'ggml/src/ggml-webgpu/wgsl-shaders/embed_wgsl.py'
|
||||
'ggml/src/ggml-webgpu/**'
|
||||
]
|
||||
|
||||
pull_request:
|
||||
@@ -63,135 +55,3 @@ jobs:
|
||||
-type f \( -name '*.cpp' -o -name '*.hpp' -o -name '*.h' \) \
|
||||
-print0 |
|
||||
xargs -0 clang-format-22 --dry-run --Werror
|
||||
|
||||
macos:
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: ccache
|
||||
uses: ggml-org/ccache-action@v1.2.24
|
||||
with:
|
||||
restore: false
|
||||
save: false
|
||||
|
||||
- name: Dawn Dependency
|
||||
id: dawn-depends
|
||||
run: |
|
||||
DAWN_VERSION="v20260908.214631"
|
||||
DAWN_OWNER="google"
|
||||
DAWN_REPO="dawn"
|
||||
DAWN_ASSET_NAME="Dawn-94c3c9cc0d5fb2e85aebb370fa8d37b71aa34655-macos-latest-Release"
|
||||
echo "Fetching release asset from https://github.com/google/dawn/releases/download/${DAWN_VERSION}/${DAWN_ASSET_NAME}.tar.gz"
|
||||
curl -L -o artifact.tar.gz \
|
||||
"https://github.com/google/dawn/releases/download/${DAWN_VERSION}/${DAWN_ASSET_NAME}.tar.gz"
|
||||
mkdir dawn
|
||||
tar -xvf artifact.tar.gz -C dawn --strip-components=1
|
||||
|
||||
- name: ccache-buckets-restore
|
||||
uses: ./.github/actions/ccache-buckets
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN_CI }}
|
||||
with:
|
||||
key: webgpu-macos-latest
|
||||
folder: llama.cpp
|
||||
hf_bucket: ggml-org/cache
|
||||
|
||||
- name: Build
|
||||
id: cmake_build
|
||||
run: |
|
||||
export CMAKE_PREFIX_PATH=dawn
|
||||
cmake -B build -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DGGML_WEBGPU=ON -DGGML_METAL=OFF -DGGML_BLAS=OFF
|
||||
time cmake --build build --config Release -j $(sysctl -n hw.logicalcpu)
|
||||
|
||||
- name: ccache-buckets-save
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||
uses: ./.github/actions/ccache-buckets
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN_CACHE_OUTPUT }}
|
||||
with:
|
||||
key: webgpu-macos-latest
|
||||
folder: llama.cpp
|
||||
evict-old-files: 1d
|
||||
hf_bucket: ggml-org/cache
|
||||
save: true
|
||||
|
||||
- name: Test
|
||||
id: cmake_test
|
||||
run: |
|
||||
cd build
|
||||
ctest -L main --verbose --timeout 900
|
||||
|
||||
ubuntu:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: ccache
|
||||
uses: ggml-org/ccache-action@v1.2.24
|
||||
with:
|
||||
restore: false
|
||||
save: false
|
||||
|
||||
- name: Dependencies
|
||||
id: depends
|
||||
run: |
|
||||
sudo add-apt-repository -y ppa:kisak/kisak-mesa
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y build-essential mesa-vulkan-drivers \
|
||||
libxcb-xinput0 libxcb-xinerama0 libxcb-cursor-dev libssl-dev
|
||||
|
||||
- name: Dawn Dependency
|
||||
id: dawn-depends
|
||||
run: |
|
||||
sudo apt-get install -y libxrandr-dev libxinerama-dev libxcursor-dev mesa-common-dev libx11-xcb-dev libxi-dev
|
||||
DAWN_VERSION="v20260908.214631"
|
||||
DAWN_OWNER="google"
|
||||
DAWN_REPO="dawn"
|
||||
DAWN_ASSET_NAME="Dawn-94c3c9cc0d5fb2e85aebb370fa8d37b71aa34655-ubuntu-latest-Release"
|
||||
echo "Fetching release asset from https://github.com/google/dawn/releases/download/${DAWN_VERSION}/${DAWN_ASSET_NAME}.tar.gz"
|
||||
curl -L -o artifact.tar.gz \
|
||||
"https://github.com/google/dawn/releases/download/${DAWN_VERSION}/${DAWN_ASSET_NAME}.tar.gz"
|
||||
mkdir dawn
|
||||
tar -xvf artifact.tar.gz -C dawn --strip-components=1
|
||||
|
||||
- name: ccache-buckets-restore
|
||||
uses: ./.github/actions/ccache-buckets
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN_CI }}
|
||||
with:
|
||||
key: webgpu-ubuntu-24.04
|
||||
folder: llama.cpp
|
||||
hf_bucket: ggml-org/cache
|
||||
|
||||
- name: Build
|
||||
id: cmake_build
|
||||
run: |
|
||||
export Dawn_DIR=dawn/lib64/cmake/Dawn
|
||||
cmake -B build \
|
||||
-DGGML_WEBGPU=ON
|
||||
time cmake --build build --config Release -j $(nproc)
|
||||
|
||||
- name: ccache-buckets-save
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||
uses: ./.github/actions/ccache-buckets
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN_CACHE_OUTPUT }}
|
||||
with:
|
||||
key: webgpu-ubuntu-24.04
|
||||
folder: llama.cpp
|
||||
evict-old-files: 1d
|
||||
hf_bucket: ggml-org/cache
|
||||
save: true
|
||||
|
||||
- name: Test
|
||||
id: cmake_test
|
||||
run: |
|
||||
cd build
|
||||
ctest -L main --verbose --timeout 900
|
||||
|
||||
@@ -1175,6 +1175,7 @@ static const std::map<common_decision_type, std::string> COMMON_DECISION_TYPE_NA
|
||||
{ COMMON_DECISION_TYPE_LFM2_D1, "lfm2-d1" },
|
||||
{ COMMON_DECISION_TYPE_LFM2_D1_OMNI, "lfm2-d1-omni" },
|
||||
{ COMMON_DECISION_TYPE_GRANITE_SWITCH, "granite-switch"},
|
||||
{ COMMON_DECISION_TYPE_H2O_LIGHTNING, "h2o-lightning" },
|
||||
// for future models, ALWAYS add org name prefix to avoid name collisions, e.g. "orgname-modelname"
|
||||
};
|
||||
|
||||
@@ -1234,6 +1235,9 @@ common_gguf_info common_get_gguf_info(const std::string & fname) {
|
||||
info.n_ctx_train = gguf_get_val_u32(gguf_ctx.get(), ctx_id);
|
||||
}
|
||||
|
||||
// same key as llama_model_chat_template(model, nullptr)
|
||||
info.has_chat_tmpl = gguf_find_key(gguf_ctx.get(), "tokenizer.chat_template") >= 0;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
|
||||
@@ -975,6 +975,7 @@ enum common_decision_type {
|
||||
COMMON_DECISION_TYPE_LFM2_D1, // same as openjev, the labels depend on the question type
|
||||
COMMON_DECISION_TYPE_LFM2_D1_OMNI, // same as laya, other prompt layout
|
||||
COMMON_DECISION_TYPE_GRANITE_SWITCH, // same as openjev, run by the decision aLoRA of granite switch
|
||||
COMMON_DECISION_TYPE_H2O_LIGHTNING, // same as openjev, the labels are read after an answer prefill
|
||||
COMMON_DECISION_TYPE_UNKNOWN, // a decision model of a type that is not supported
|
||||
};
|
||||
|
||||
@@ -984,6 +985,7 @@ common_decision_type common_get_decision_type(const struct llama_model * model);
|
||||
struct common_gguf_info {
|
||||
common_decision_type decision_type = COMMON_DECISION_TYPE_UNKNOWN; // UNKNOWN if the file is missing, unreadable, or invalid
|
||||
uint32_t n_ctx_train = 0; // 0 if unknown
|
||||
bool has_chat_tmpl = false; // has the default tokenizer.chat_template
|
||||
};
|
||||
|
||||
common_gguf_info common_get_gguf_info(const std::string & fname);
|
||||
|
||||
@@ -200,7 +200,9 @@ TEXT_MODEL_MAP: dict[str, str] = {
|
||||
"MiniMaxM3SparseForCausalLM": "minimax",
|
||||
"MiniMaxM3SparseForConditionalGeneration": "minimax",
|
||||
"Ministral3ForCausalLM": "mistral3",
|
||||
"Ministral3Model": "mistral3",
|
||||
"Mistral3ForConditionalGeneration": "mistral3",
|
||||
"Mistral3Model": "mistral3",
|
||||
"VoxtralRealtimeForConditionalGeneration": "mistral3",
|
||||
"MistralForCausalLM": "llama",
|
||||
"MixtralForCausalLM": "llama",
|
||||
@@ -226,6 +228,7 @@ TEXT_MODEL_MAP: dict[str, str] = {
|
||||
"MuseSparkForConditionalGeneration": "muse_spark",
|
||||
"OpenELMForCausalLM": "openelm",
|
||||
"OpenJevModel": "qwen",
|
||||
"H2OLightningModel": "h2o_lightning",
|
||||
"OrionForCausalLM": "orion",
|
||||
"PLMForCausalLM": "plm",
|
||||
"PLaMo2ForCausalLM": "plamo",
|
||||
@@ -357,6 +360,7 @@ MMPROJ_MODEL_MAP: dict[str, str] = {
|
||||
"MiniCPMV4_6ForConditionalGeneration": "minicpm",
|
||||
"MiniCPMV4_7ForConditionalGeneration": "minicpm",
|
||||
"Mistral3ForConditionalGeneration": "llava",
|
||||
"Mistral3Model": "llava",
|
||||
"NemotronH_Omni_Reasoning_V3": "nemotron",
|
||||
"NemotronH_Nano_VL_V2": "nemotron",
|
||||
"MuseGlimmerForConditionalGeneration": "muse_glimmer",
|
||||
@@ -375,6 +379,7 @@ MMPROJ_MODEL_MAP: dict[str, str] = {
|
||||
"Qwen3VLForConditionalGeneration": "qwen3vl",
|
||||
"Qwen3VLMoeForConditionalGeneration": "qwen3vl",
|
||||
"OpenJevModel": "qwen3vl",
|
||||
"H2OLightningModel": "h2o_lightning",
|
||||
"Qwen3_5ForConditionalGeneration": "qwen3vl",
|
||||
"Qwen3_5MoeForConditionalGeneration": "qwen3vl",
|
||||
"Qwen4ExpForConditionalGeneration": "qwen4exp",
|
||||
|
||||
@@ -999,6 +999,8 @@ class Gemma4VisionAudioModel(MmprojModel):
|
||||
assert self.hparams_vision is not None
|
||||
self.gguf_writer.add_clip_vision_projector_type(gguf.VisionProjectorType.GEMMA4V)
|
||||
self.gguf_writer.add_vision_attention_layernorm_eps(self.hparams_vision.get("layer_norm_eps", 1e-6))
|
||||
assert self.hparams_vision.get("hidden_activation", "gelu_pytorch_tanh") == "gelu_pytorch_tanh"
|
||||
self.gguf_writer.add_vision_use_gelu(True)
|
||||
|
||||
# audio params
|
||||
if self.has_audio_encoder:
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .base import ModelBase, gguf, logger
|
||||
from .qwen import Qwen3_5TextModel
|
||||
from .qwen3vl import Qwen3VLVisionModel
|
||||
|
||||
|
||||
def _is_h2o_lightning_checkpoint(dir_model: Path) -> bool:
|
||||
return (dir_model / "h2o_lightning_shim.py").is_file() and (dir_model / "serve_config.json").is_file()
|
||||
|
||||
|
||||
@ModelBase.register_hparams_loader(_is_h2o_lightning_checkpoint)
|
||||
def _load_h2o_lightning_hparams(dir_model: Path) -> dict[str, Any]:
|
||||
logger.info("gguf: detected H2O-Lightning checkpoint")
|
||||
hparams = ModelBase.load_hparams(dir_model, False, guess=False)
|
||||
hparams["architectures"] = ["H2OLightningModel"]
|
||||
return hparams
|
||||
|
||||
|
||||
@ModelBase.register("H2OLightningModel")
|
||||
@ModelBase.example("h2oai/h2o-lightning-4b")
|
||||
class H2OLightningModel(Qwen3_5TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.QWEN35
|
||||
|
||||
# prompt and calibration follow serve_config.json and h2o_lightning_shim.py of the model repo
|
||||
# the state budget is hard-coded in server-decision.cpp
|
||||
_MAX_STATE_TOKENS = 32000
|
||||
_DEFAULT_INSTRUCTIONS = "Answer the question below."
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
with open(self.dir_model / "serve_config.json", "r", encoding="utf-8") as f:
|
||||
self._serve_config = json.load(f)
|
||||
prompt = self._serve_config["prompt"]
|
||||
expected = {
|
||||
"state_compact": (prompt.get("state_compact", True), True),
|
||||
"max_state_tokens": (prompt.get("max_state_tokens"), self._MAX_STATE_TOKENS),
|
||||
"layout": (prompt.get("layout", "plain"), "plain"),
|
||||
"chat_template": (prompt.get("chat_template", True), True),
|
||||
"enable_thinking": (prompt.get("enable_thinking", False), False),
|
||||
}
|
||||
for name, (got, want) in expected.items():
|
||||
if got != want:
|
||||
raise ValueError(f"serve_config.json: {name} is {got!r}, the h2o-lightning decision type expects {want!r}")
|
||||
|
||||
def get_vocab_base_pre(self, tokenizer) -> str:
|
||||
# transformers loads this checkpoint as Qwen2Tokenizer: the model was trained and is served with the Qwen2 split
|
||||
pre = super().get_vocab_base_pre(tokenizer)
|
||||
if pre != "qwen35":
|
||||
raise ValueError(f"expected the Qwen3.5 tokenizer, got pre-tokenizer {pre!r}")
|
||||
return "qwen2"
|
||||
|
||||
def set_vocab(self):
|
||||
super().set_vocab()
|
||||
self.gguf_writer.add_chat_template([{"name": "systemone", "template": self._systemone_template()}])
|
||||
|
||||
def _systemone_template(self) -> str:
|
||||
# the server gives the template texts rendered as the shim renders them, it only lays them out
|
||||
prompt = self._serve_config["prompt"]
|
||||
nl = "{{ '\\n' }}"
|
||||
description = (
|
||||
"{% if type == 'noul' %}"
|
||||
"{% if o.description %}{{ o.description }}{% elif o.key == 'true' %}the statement holds{% else %}it does not{% endif %}"
|
||||
"{% elif o.description is none %}{{ o.key }}{% else %}{{ o.description }}{% endif %}"
|
||||
)
|
||||
user = (
|
||||
"{% set user %}record: {{ state }}" + nl
|
||||
+ "question: {{ instructions if instructions else '" + self._DEFAULT_INSTRUCTIONS + "' }}" + nl
|
||||
+ "options:{% for o in options %}" + nl + "{{ o.label | trim }}) {{ o.key }}: " + description + "{% endfor %}{% endset %}"
|
||||
)
|
||||
return (
|
||||
"<|im_start|>system" + nl + prompt["system"].strip() + "<|im_end|>" + nl
|
||||
+ "<|im_start|>user" + nl + "{% for image in images %}{{ image }}{% endfor %}" + user + "{{ user | trim }}<|im_end|>" + nl
|
||||
+ "<|im_start|>assistant" + nl + "<think>{{ '\\n\\n' }}</think>{{ '\\n\\n' }}" + prompt["prefill"]
|
||||
)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
self.gguf_writer.add_decision_type(gguf.DecisionType.H2O_LIGHTNING)
|
||||
# the answer follows "Answer:", so the token of a label has a leading space
|
||||
cfg = self._serve_config
|
||||
self.gguf_writer.add_decision_labels([" " + label for label in cfg["labels"][:cfg.get("max_options", len(cfg["labels"]))]])
|
||||
for name in ("choice", "noul", "score"):
|
||||
self.gguf_writer.add_decision_temperature(name, self._serve_config["temperature"][name])
|
||||
# a request with images has this temperature for all question types
|
||||
self.gguf_writer.add_decision_temperature("image", self._serve_config["image"]["temperature"])
|
||||
|
||||
|
||||
@ModelBase.register("H2OLightningModel")
|
||||
class H2OLightningVisionModel(Qwen3VLVisionModel):
|
||||
pass
|
||||
@@ -374,6 +374,16 @@ class LFM2VLModel(MmprojModel):
|
||||
if resize_algo is None:
|
||||
raise ValueError(f"unsupported resample: {resample}")
|
||||
self.gguf_writer.add_vision_image_resize_algo(resize_algo)
|
||||
# tiling params from processor_config.json (do_image_splitting=False -> single tile, i.e. no tiling)
|
||||
preproc = self.preprocessor_config
|
||||
if preproc.get("do_image_splitting", True):
|
||||
self.gguf_writer.add_vision_preproc_min_tiles(preproc.get("min_tiles", 2))
|
||||
self.gguf_writer.add_vision_preproc_max_tiles(preproc.get("max_tiles", 10))
|
||||
else:
|
||||
self.gguf_writer.add_vision_preproc_min_tiles(1)
|
||||
self.gguf_writer.add_vision_preproc_max_tiles(1)
|
||||
self.gguf_writer.add_vision_preproc_tile_size(preproc.get("tile_size", 512))
|
||||
self.gguf_writer.add_vision_preproc_max_pixels_tolerance(preproc.get("max_pixels_tolerance", 2.0))
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
||||
|
||||
+22
-1
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from typing import Iterable, TYPE_CHECKING
|
||||
from typing import Callable, Iterable, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch import Tensor
|
||||
@@ -128,3 +128,24 @@ class LlavaVisionModel(MmprojModel):
|
||||
yield from super().modify_tensors(img_break_embd, name, bid)
|
||||
|
||||
return # skip other tensors
|
||||
|
||||
|
||||
@ModelBase.register("Mistral3Model")
|
||||
@ModelBase.example("nvidia/Nemotron-3.5-Embed-1B-BF16")
|
||||
class Mistral3EmbeddingVisionModel(LlavaVisionModel):
|
||||
def __init__(self, dir_model, *args, **kwargs):
|
||||
hparams = ModelBase.load_hparams(dir_model, is_mistral_format=False)
|
||||
text_config = hparams.get("text_config", hparams)
|
||||
if text_config.get("is_causal") is not False:
|
||||
raise NotImplementedError("Expected a Nemotron Embed model for Mistral3Model, but is_causal is not False")
|
||||
super().__init__(dir_model, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
||||
if item[0].endswith("embed_tokens.weight"):
|
||||
return ModelBase.filter_tensors(item)
|
||||
return super().filter_tensors(item)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
self.gguf_writer.add_vision_preproc_image_size(self.preprocessor_config["size"]["longest_edge"])
|
||||
|
||||
+28
-6
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from torch import Tensor
|
||||
|
||||
from .base import ModelBase, TextModel, gguf
|
||||
from .base import ModelBase, TextModel, get_model_architecture, gguf
|
||||
|
||||
from .deepseek import DeepseekV2Model
|
||||
from .llama import LlamaModel
|
||||
@@ -13,9 +13,11 @@ from .llama import LlamaModel
|
||||
|
||||
@ModelBase.register(
|
||||
"Mistral3ForConditionalGeneration",
|
||||
"Mistral3Model",
|
||||
"Ministral3ForCausalLM",
|
||||
"Ministral3Model",
|
||||
)
|
||||
@ModelBase.example("mistralai/Mistral-Small-3.1-24B-Instruct-2503", "hf-tiny-v2/tiny-random-Ministral3ForCausalLM")
|
||||
@ModelBase.example("mistralai/Mistral-Small-3.1-24B-Instruct-2503", "hf-tiny-v2/tiny-random-Ministral3ForCausalLM", "nvidia/Nemotron-3-Embed-1B-BF16", "nvidia/Nemotron-3.5-Embed-1B-BF16")
|
||||
class Mistral3Model(TextModel):
|
||||
class Ministral3Model(LlamaModel):
|
||||
model_arch = gguf.MODEL_ARCH.MISTRAL3
|
||||
@@ -29,6 +31,18 @@ class Mistral3Model(TextModel):
|
||||
self.gguf_writer.add_rope_scaling_yarn_log_mul(rope_params["mscale_all_dim"])
|
||||
self.gguf_writer.add_attn_temperature_scale(rope_params["llama_4_scaling_beta"])
|
||||
|
||||
class Mistral3EmbedNemotronModel(Ministral3Model):
|
||||
model_arch = gguf.MODEL_ARCH.MISTRAL3
|
||||
|
||||
def set_vocab(self):
|
||||
super().set_vocab()
|
||||
self.gguf_writer.add_add_bos_token(False)
|
||||
self.gguf_writer.add_add_eos_token(False)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
self._try_set_pooling_type()
|
||||
|
||||
class Mistral4Model(DeepseekV2Model):
|
||||
model_arch = gguf.MODEL_ARCH.MISTRAL4
|
||||
skip_mtp = False # model contains no MTP layers, so no need to skip
|
||||
@@ -42,12 +56,20 @@ class Mistral3Model(TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.MISTRAL3 # unused
|
||||
impl: TextModel
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
def __init__(self, dir_model, *args, **kwargs):
|
||||
hparams = ModelBase.load_hparams(dir_model, is_mistral_format=False)
|
||||
hf_arch = get_model_architecture(hparams, self.model_type)
|
||||
if hf_arch in ("Ministral3Model", "Mistral3Model"):
|
||||
text_config = hparams.get("text_config", hparams)
|
||||
if text_config.get("is_causal") is not False:
|
||||
raise NotImplementedError(f"Expected a Nemotron Embed model for {hf_arch}, but is_causal is not False")
|
||||
super().__init__(dir_model, *args, **kwargs)
|
||||
if self.hparams.get("model_type") == "mistral4":
|
||||
self.impl = Mistral3Model.Mistral4Model(*args, **kwargs)
|
||||
self.impl = Mistral3Model.Mistral4Model(dir_model, *args, **kwargs)
|
||||
elif self.hf_arch in ("Ministral3Model", "Mistral3Model"):
|
||||
self.impl = Mistral3Model.Mistral3EmbedNemotronModel(dir_model, *args, **kwargs)
|
||||
else:
|
||||
self.impl = Mistral3Model.Ministral3Model(*args, **kwargs)
|
||||
self.impl = Mistral3Model.Ministral3Model(dir_model, *args, **kwargs)
|
||||
|
||||
def set_vocab(self):
|
||||
self.impl.set_vocab()
|
||||
|
||||
@@ -2226,6 +2226,7 @@ bool ggml_op_alloc_size_may_expand(enum ggml_op op) {
|
||||
case GGML_OP_CUMSUM:
|
||||
case GGML_OP_ARGSORT:
|
||||
case GGML_OP_TOP_K:
|
||||
case GGML_OP_ARGMAX:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
||||
@@ -42,6 +42,7 @@ extern "C" {
|
||||
#define GGML_CPU_COMPUTE_FP32_TO_FP16(x) neon_compute_fp32_to_fp16(x)
|
||||
|
||||
#define GGML_CPU_FP16_TO_FP32(x) GGML_CPU_COMPUTE_FP16_TO_FP32(x)
|
||||
#define GGML_CPU_FP32_TO_FP16(x) GGML_CPU_COMPUTE_FP32_TO_FP16(x)
|
||||
|
||||
static inline float neon_compute_fp16_to_fp32(ggml_fp16_t h) {
|
||||
__fp16 tmp;
|
||||
@@ -150,9 +151,15 @@ extern float ggml_table_f32_ue4m3[1 << 8];
|
||||
#define GGML_CPU_FP32_TO_FP16(x) GGML_CPU_COMPUTE_FP32_TO_FP16(x)
|
||||
#endif
|
||||
|
||||
// On ARM NEON, it's quicker to directly convert x -> x instead of calling into ggml_lookup_fp16_to_fp32,
|
||||
// so we define GGML_CPU_FP16_TO_FP32 and GGML_CPU_FP32_TO_FP16 elsewhere for NEON.
|
||||
// This is also true for POWER9.
|
||||
// a branch that defines a GGML_CPU_COMPUTE_* macro must also select it, or the fallback takes over silently
|
||||
#if defined(GGML_CPU_COMPUTE_FP16_TO_FP32) && !defined(GGML_CPU_FP16_TO_FP32) && !defined(__F16C__) // see: https://github.com/ggml-org/llama.cpp/issues/30342
|
||||
#error "a branch defined GGML_CPU_COMPUTE_FP16_TO_FP32 without selecting it as GGML_CPU_FP16_TO_FP32"
|
||||
#endif
|
||||
#if defined(GGML_CPU_COMPUTE_FP32_TO_FP16) && !defined(GGML_CPU_FP32_TO_FP16)
|
||||
#error "a branch defined GGML_CPU_COMPUTE_FP32_TO_FP16 without selecting it as GGML_CPU_FP32_TO_FP16"
|
||||
#endif
|
||||
|
||||
// fall back to the lookup table / the portable software conversion for whichever direction no branch selected
|
||||
#if !defined(GGML_CPU_FP16_TO_FP32)
|
||||
inline static float ggml_lookup_fp16_to_fp32(ggml_fp16_t f) {
|
||||
uint16_t s;
|
||||
|
||||
@@ -151,9 +151,10 @@ static void ssm_conv_f32_cuda(const float * src0, const float * src1, const floa
|
||||
case 3: launch_kernel(std::integral_constant<int, 3 >{}); break;
|
||||
case 4: launch_kernel(std::integral_constant<int, 4 >{}); break;
|
||||
case 5: launch_kernel(std::integral_constant<int, 5 >{}); break;
|
||||
case 7: launch_kernel(std::integral_constant<int, 7 >{}); break;
|
||||
case 9: launch_kernel(std::integral_constant<int, 9 >{}); break;
|
||||
case 15: launch_kernel(std::integral_constant<int, 15>{}); break;
|
||||
default: GGML_ABORT("Only support kernel sizes 3, 4, 5, 9, 15 right now.");
|
||||
default: GGML_ABORT("Only support kernel sizes 3, 4, 5, 7, 9, 15 right now.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -316,9 +316,17 @@ static int vtcm_release_callback(unsigned int rctx, void * state) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int vtcm_alloc(struct htp_context * ctx) {
|
||||
static unsigned int vtcm_query_size(void) {
|
||||
unsigned int vtcm_size = 8 * 1024 * 1024; // 8MB default
|
||||
HAP_compute_res_query_VTCM(0, &vtcm_size, NULL, NULL, NULL);
|
||||
unsigned int vtcm_avail;
|
||||
compute_res_vtcm_page_t total_layout, avail_layout;
|
||||
// some images (e.g. FuSa cDSP) fail the query when the layout out-params are NULL
|
||||
HAP_compute_res_query_VTCM(0, &vtcm_size, &total_layout, &vtcm_avail, &avail_layout);
|
||||
return vtcm_size;
|
||||
}
|
||||
|
||||
static int vtcm_alloc(struct htp_context * ctx) {
|
||||
unsigned int vtcm_size = vtcm_query_size();
|
||||
|
||||
compute_res_attr_t attr;
|
||||
HAP_compute_res_attr_init(&attr);
|
||||
@@ -770,9 +778,7 @@ AEEResult htp_iface_hwinfo(remote_handle64 handle, uint32_t * n_threads, uint32_
|
||||
*n_hvx = n_hvx_val;
|
||||
*n_hmx = 1;
|
||||
|
||||
uint32_t vtcm_sz = 8 * 1024 * 1024; // 8MB default fallback
|
||||
HAP_compute_res_query_VTCM(0, (unsigned int *)&vtcm_sz, NULL, NULL, NULL);
|
||||
*vtcm_size = vtcm_sz;
|
||||
*vtcm_size = vtcm_query_size();
|
||||
|
||||
return AEE_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -1567,6 +1567,28 @@ ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_argmax(ggml_meta
|
||||
return res;
|
||||
}
|
||||
|
||||
ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_argmax_part(ggml_metal_library_t lib) {
|
||||
ggml_metal_pipeline_with_params res = ggml_metal_library_get_pipeline(lib, "kernel_argmax_part_f32");
|
||||
if (!res.pipeline) {
|
||||
res = ggml_metal_library_compile_pipeline(lib, "kernel_argmax_part_f32", "kernel_argmax_part_f32", nullptr);
|
||||
}
|
||||
|
||||
res.smem = 32*(sizeof(float) + sizeof(int32_t));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_argmax_final(ggml_metal_library_t lib) {
|
||||
ggml_metal_pipeline_with_params res = ggml_metal_library_get_pipeline(lib, "kernel_argmax_final_f32");
|
||||
if (!res.pipeline) {
|
||||
res = ggml_metal_library_compile_pipeline(lib, "kernel_argmax_final_f32", "kernel_argmax_final_f32", nullptr);
|
||||
}
|
||||
|
||||
res.smem = 32*(sizeof(float) + sizeof(int32_t));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_argsort(ggml_metal_library_t lib, const ggml_tensor * op) {
|
||||
assert(op->op == GGML_OP_ARGSORT);
|
||||
|
||||
|
||||
@@ -145,6 +145,8 @@ struct ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_mul_mm_id
|
||||
struct ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_mul_mm_id (ggml_metal_library_t lib, const struct ggml_tensor * op);
|
||||
struct ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_mul_mv_id (ggml_metal_library_t lib, const struct ggml_tensor * op);
|
||||
struct ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_argmax (ggml_metal_library_t lib, const struct ggml_tensor * op);
|
||||
struct ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_argmax_part (ggml_metal_library_t lib);
|
||||
struct ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_argmax_final (ggml_metal_library_t lib);
|
||||
struct ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_argsort (ggml_metal_library_t lib, const struct ggml_tensor * op);
|
||||
struct ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_argsort_merge (ggml_metal_library_t lib, const struct ggml_tensor * op);
|
||||
struct ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_fwht (ggml_metal_library_t lib, int n, enum ggml_type tsrc);
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
#define N_MM_NPART_AMAX 256
|
||||
|
||||
#define N_ARGMAX_NPART_MAX 256
|
||||
#define N_ARGMAX_CHUNK 4096
|
||||
|
||||
// kernel parameters for mat-vec threadgroups
|
||||
//
|
||||
// N_R0: number of src0 rows to process per simdgroup
|
||||
@@ -1379,8 +1382,10 @@ typedef struct {
|
||||
} ggml_metal_kargs_pool_1d;
|
||||
|
||||
typedef struct {
|
||||
int64_t ne00;
|
||||
int64_t ne00;
|
||||
uint64_t nb01;
|
||||
int64_t npart;
|
||||
int64_t ne01;
|
||||
} ggml_metal_kargs_argmax;
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -2319,13 +2319,18 @@ int ggml_metal_op_pool_1d(ggml_metal_op_t ctx, int idx) {
|
||||
|
||||
auto pipeline = ggml_metal_library_get_pipeline_pool_1d(lib, op, op_pool);
|
||||
|
||||
const int nth = std::min(ggml_metal_pipeline_max_theads_per_threadgroup(pipeline), (int) np);
|
||||
const int ntg = (np + nth - 1) / nth;
|
||||
const int nth = std::min(256, std::max(1, k0));
|
||||
const int ntg = (int) np;
|
||||
|
||||
const size_t smem = op_pool == GGML_OP_POOL_AVG ?
|
||||
2*32*sizeof(float) :
|
||||
32*sizeof(float);
|
||||
|
||||
ggml_metal_encoder_set_pipeline(enc, pipeline);
|
||||
ggml_metal_encoder_set_bytes (enc, &args_pool_1d, sizeof(args_pool_1d), 0);
|
||||
ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op->src[0]), 1);
|
||||
ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op), 2);
|
||||
ggml_metal_encoder_set_threadgroup_memory_size(enc, smem, 0);
|
||||
|
||||
ggml_metal_encoder_dispatch_threadgroups(enc, ntg, 1, 1, nth, 1, 1);
|
||||
|
||||
@@ -5421,6 +5426,25 @@ int ggml_metal_op_timestep_embedding(ggml_metal_op_t ctx, int idx) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t ggml_metal_op_argmax_extra(const ggml_tensor * op) {
|
||||
assert(op->op == GGML_OP_ARGMAX);
|
||||
|
||||
const int64_t ne00 = op->src[0]->ne[0];
|
||||
const int64_t ne01 = op->src[0]->ne[1];
|
||||
|
||||
int npart = 1;
|
||||
if (ne00 > N_ARGMAX_CHUNK) {
|
||||
npart = (int) std::min<int64_t>(N_ARGMAX_NPART_MAX, (ne00 + N_ARGMAX_CHUNK - 1)/N_ARGMAX_CHUNK);
|
||||
}
|
||||
|
||||
if (npart == 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// partial max + partial argmax for every row
|
||||
return (sizeof(float) + sizeof(int32_t)) * npart * ne01;
|
||||
}
|
||||
|
||||
int ggml_metal_op_argmax(ggml_metal_op_t ctx, int idx) {
|
||||
ggml_tensor * op = ctx->node(idx);
|
||||
|
||||
@@ -5432,22 +5456,61 @@ int ggml_metal_op_argmax(ggml_metal_op_t ctx, int idx) {
|
||||
GGML_TENSOR_LOCALS( int32_t, ne, op, ne);
|
||||
GGML_TENSOR_LOCALS(uint64_t, nb, op, nb);
|
||||
|
||||
const int64_t nrows = ggml_nrows(op->src[0]);
|
||||
|
||||
int npart = 1;
|
||||
if (ne00 > N_ARGMAX_CHUNK) {
|
||||
npart = (int) std::min<int64_t>(N_ARGMAX_NPART_MAX, (ne00 + N_ARGMAX_CHUNK - 1)/N_ARGMAX_CHUNK);
|
||||
}
|
||||
|
||||
ggml_metal_kargs_argmax args = {
|
||||
/*.ne00 = */ ne00,
|
||||
/*.nb01 = */ nb01,
|
||||
/*.ne00 = */ ne00,
|
||||
/*.nb01 = */ nb01,
|
||||
/*.npart = */ npart,
|
||||
/*.ne01 = */ ne01,
|
||||
};
|
||||
|
||||
auto pipeline = ggml_metal_library_get_pipeline_argmax(lib, op);
|
||||
const size_t smem = 32*(sizeof(float) + sizeof(int32_t));
|
||||
|
||||
const int64_t nrows = ggml_nrows(op->src[0]);
|
||||
if (npart > 1) {
|
||||
auto pipeline_part = ggml_metal_library_get_pipeline_argmax_part(lib);
|
||||
auto pipeline_final = ggml_metal_library_get_pipeline_argmax_final(lib);
|
||||
|
||||
ggml_metal_buffer_id bid_dst = ggml_metal_get_buffer_id(op);
|
||||
ggml_metal_buffer_id bid_tmp = bid_dst;
|
||||
bid_tmp.offs += ggml_nbytes(op);
|
||||
|
||||
ggml_metal_encoder_set_pipeline(enc, pipeline_part);
|
||||
ggml_metal_encoder_set_bytes (enc, &args, sizeof(args), 0);
|
||||
ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op->src[0]), 1);
|
||||
ggml_metal_encoder_set_buffer (enc, bid_tmp, 2);
|
||||
ggml_metal_encoder_set_threadgroup_memory_size(enc, smem, 0);
|
||||
ggml_metal_encoder_dispatch_threadgroups(enc, npart, nrows, 1, 256, 1, 1);
|
||||
|
||||
ggml_metal_op_concurrency_reset(ctx);
|
||||
|
||||
int nth = 32; // SIMD width
|
||||
while (nth < npart && nth < 256) {
|
||||
nth *= 2;
|
||||
}
|
||||
|
||||
ggml_metal_encoder_set_pipeline(enc, pipeline_final);
|
||||
ggml_metal_encoder_set_bytes (enc, &args, sizeof(args), 0);
|
||||
ggml_metal_encoder_set_buffer (enc, bid_tmp, 1);
|
||||
ggml_metal_encoder_set_buffer (enc, bid_dst, 2);
|
||||
ggml_metal_encoder_set_threadgroup_memory_size(enc, smem, 0);
|
||||
ggml_metal_encoder_dispatch_threadgroups(enc, nrows, 1, 1, nth, 1, 1);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto pipeline = ggml_metal_library_get_pipeline_argmax(lib, op);
|
||||
|
||||
int nth = 32; // SIMD width
|
||||
while (nth < ne00 && nth*ne01*ne02*ne03 < 256) {
|
||||
nth *= 2;
|
||||
}
|
||||
|
||||
const size_t smem = pipeline.smem;
|
||||
|
||||
ggml_metal_encoder_set_pipeline(enc, pipeline);
|
||||
ggml_metal_encoder_set_bytes (enc, &args, sizeof(args), 0);
|
||||
ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op->src[0]), 1);
|
||||
|
||||
@@ -47,6 +47,8 @@ size_t ggml_metal_op_flash_attn_ext_extra_tmp(const struct ggml_tensor * op);
|
||||
size_t ggml_metal_op_flash_attn_ext_extra_kv_f16(const struct ggml_tensor * op);
|
||||
size_t ggml_metal_op_flash_attn_ext_extra_idx(const struct ggml_tensor * op);
|
||||
|
||||
size_t ggml_metal_op_argmax_extra(const struct ggml_tensor * op);
|
||||
|
||||
int ggml_metal_op_concat (ggml_metal_op_t ctx, int idx);
|
||||
int ggml_metal_op_repeat (ggml_metal_op_t ctx, int idx);
|
||||
int ggml_metal_op_acc (ggml_metal_op_t ctx, int idx);
|
||||
|
||||
@@ -245,6 +245,10 @@ static size_t ggml_backend_metal_buffer_type_get_alloc_size(ggml_backend_buffer_
|
||||
{
|
||||
res = 2*sizeof(int32_t)*ggml_nelements(tensor->src[0]);
|
||||
} break;
|
||||
case GGML_OP_ARGMAX:
|
||||
{
|
||||
res += ggml_metal_op_argmax_extra(tensor);
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -60,6 +60,140 @@ kernel void kernel_argmax_f32(
|
||||
dst_i32[tgpig] = arg_val;
|
||||
}
|
||||
|
||||
kernel void kernel_argmax_part_f32(
|
||||
constant ggml_metal_kargs_argmax & args,
|
||||
device const char * src0,
|
||||
device char * tmp,
|
||||
threadgroup char * shmem [[threadgroup(0)]],
|
||||
uint3 tgpig[[threadgroup_position_in_grid]],
|
||||
uint3 tpitg[[thread_position_in_threadgroup]],
|
||||
uint sgitg[[simdgroup_index_in_threadgroup]],
|
||||
uint tiisg[[thread_index_in_simdgroup]],
|
||||
uint3 ntg[[threads_per_threadgroup]]) {
|
||||
const int row = tgpig.y;
|
||||
const int part = tgpig.x;
|
||||
const int ne00 = (int) args.ne00;
|
||||
const int npart = (int) args.npart;
|
||||
const int chunk = (ne00 + npart - 1) / npart;
|
||||
const int start = part * chunk;
|
||||
const int end = min(start + chunk, ne00);
|
||||
|
||||
device const float * x_row = (device const float *) ((device const char *) src0 + row * args.nb01);
|
||||
|
||||
float lmax = -INFINITY;
|
||||
int32_t larg = -1;
|
||||
|
||||
for (int i00 = start + tpitg.x; i00 < end; i00 += ntg.x) {
|
||||
if (x_row[i00] > lmax) {
|
||||
lmax = x_row[i00];
|
||||
larg = i00;
|
||||
}
|
||||
}
|
||||
|
||||
// find the argmax value in the block
|
||||
float max_val = simd_max(lmax);
|
||||
int32_t arg_val = simd_max(select(-1, larg, lmax == max_val));
|
||||
|
||||
threadgroup float * shared_maxval = (threadgroup float *) shmem;
|
||||
threadgroup int32_t * shared_argmax = (threadgroup int32_t *) shmem + N_SIMDWIDTH;
|
||||
|
||||
if (ntg.x > N_SIMDWIDTH) {
|
||||
if (sgitg == 0) {
|
||||
shared_maxval[tiisg] = -INFINITY;
|
||||
shared_argmax[tiisg] = -1;
|
||||
}
|
||||
|
||||
threadgroup_barrier(mem_flags::mem_threadgroup);
|
||||
|
||||
if (tiisg == 0) {
|
||||
shared_maxval[sgitg] = max_val;
|
||||
shared_argmax[sgitg] = arg_val;
|
||||
}
|
||||
|
||||
threadgroup_barrier(mem_flags::mem_threadgroup);
|
||||
|
||||
max_val = shared_maxval[tiisg];
|
||||
arg_val = shared_argmax[tiisg];
|
||||
|
||||
float max_val_reduced = simd_max(max_val);
|
||||
int32_t arg_val_reduced = simd_max(select(-1, arg_val, max_val == max_val_reduced));
|
||||
|
||||
max_val = max_val_reduced;
|
||||
arg_val = arg_val_reduced;
|
||||
}
|
||||
|
||||
if (tpitg.x == 0) {
|
||||
device float * tmp_max = (device float *) tmp;
|
||||
device int32_t * tmp_arg = (device int32_t *) tmp + npart * (int) args.ne01;
|
||||
|
||||
tmp_max[row*npart + part] = max_val;
|
||||
tmp_arg[row*npart + part] = arg_val;
|
||||
}
|
||||
}
|
||||
|
||||
kernel void kernel_argmax_final_f32(
|
||||
constant ggml_metal_kargs_argmax & args,
|
||||
device const char * tmp,
|
||||
device char * dst,
|
||||
threadgroup char * shmem [[threadgroup(0)]],
|
||||
uint3 tgpig[[threadgroup_position_in_grid]],
|
||||
uint3 tpitg[[thread_position_in_threadgroup]],
|
||||
uint sgitg[[simdgroup_index_in_threadgroup]],
|
||||
uint tiisg[[thread_index_in_simdgroup]],
|
||||
uint3 ntg[[threads_per_threadgroup]]) {
|
||||
const int row = tgpig.x;
|
||||
const int npart = (int) args.npart;
|
||||
|
||||
device const float * tmp_max = (device const float *) tmp;
|
||||
device const int32_t * tmp_arg = (device const int32_t *) tmp + npart * (int) args.ne01;
|
||||
|
||||
float lmax = -INFINITY;
|
||||
int32_t larg = -1;
|
||||
|
||||
for (int i = tpitg.x; i < npart; i += ntg.x) {
|
||||
if (tmp_max[row*npart + i] > lmax) {
|
||||
lmax = tmp_max[row*npart + i];
|
||||
larg = tmp_arg[row*npart + i];
|
||||
}
|
||||
}
|
||||
|
||||
// find the argmax value in the block
|
||||
float max_val = simd_max(lmax);
|
||||
int32_t arg_val = simd_max(select(-1, larg, lmax == max_val));
|
||||
|
||||
threadgroup float * shared_maxval = (threadgroup float *) shmem;
|
||||
threadgroup int32_t * shared_argmax = (threadgroup int32_t *) shmem + N_SIMDWIDTH;
|
||||
|
||||
if (ntg.x > N_SIMDWIDTH) {
|
||||
if (sgitg == 0) {
|
||||
shared_maxval[tiisg] = -INFINITY;
|
||||
shared_argmax[tiisg] = -1;
|
||||
}
|
||||
|
||||
threadgroup_barrier(mem_flags::mem_threadgroup);
|
||||
|
||||
if (tiisg == 0) {
|
||||
shared_maxval[sgitg] = max_val;
|
||||
shared_argmax[sgitg] = arg_val;
|
||||
}
|
||||
|
||||
threadgroup_barrier(mem_flags::mem_threadgroup);
|
||||
|
||||
max_val = shared_maxval[tiisg];
|
||||
arg_val = shared_argmax[tiisg];
|
||||
|
||||
float max_val_reduced = simd_max(max_val);
|
||||
int32_t arg_val_reduced = simd_max(select(-1, arg_val, max_val == max_val_reduced));
|
||||
|
||||
max_val = max_val_reduced;
|
||||
arg_val = arg_val_reduced;
|
||||
}
|
||||
|
||||
if (tpitg.x == 0) {
|
||||
((device int32_t *) dst)[row] = arg_val;
|
||||
}
|
||||
}
|
||||
|
||||
kernel void kernel_diag_f32(
|
||||
constant ggml_metal_kargs_diag & args,
|
||||
device const char * src0,
|
||||
|
||||
@@ -84,65 +84,117 @@ kernel void kernel_pool_1d_max_f32(
|
||||
constant ggml_metal_kargs_pool_1d & args,
|
||||
device const float * src,
|
||||
device float * dst,
|
||||
uint gid [[thread_position_in_grid]]
|
||||
) {
|
||||
|
||||
if (gid >= args.np) {
|
||||
threadgroup char * shmem [[threadgroup(0)]],
|
||||
uint tgpig[[threadgroup_position_in_grid]],
|
||||
uint tpitg[[thread_position_in_threadgroup]],
|
||||
uint sgitg[[simdgroup_index_in_threadgroup]],
|
||||
uint tiisg[[thread_index_in_simdgroup]],
|
||||
uint ntg[[threads_per_threadgroup]]) {
|
||||
if (tgpig >= args.np) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int ow = (int)gid % args.OW;
|
||||
const int row = (int)gid / args.OW;
|
||||
const int out = (int) tgpig;
|
||||
const int ow = out % (int) args.OW;
|
||||
const int row = out / (int) args.OW;
|
||||
|
||||
const int base = ow * args.s0 - args.p0;
|
||||
const int base = ow * args.s0 - args.p0;
|
||||
const int src_off = row * args.IW;
|
||||
const int dst_off = row * args.OW;
|
||||
|
||||
float acc = -INFINITY;
|
||||
float lmax = -INFINITY;
|
||||
|
||||
const int src_off = row * args.IW;
|
||||
const int dst_off = row * args.OW;
|
||||
|
||||
for (int ki = 0; ki < args.k0; ++ki) {
|
||||
int j = base + ki;
|
||||
if (j < 0 || j >= args.IW){
|
||||
continue;
|
||||
for (int ki = tpitg; ki < args.k0; ki += ntg) {
|
||||
const int j = base + ki;
|
||||
if (j >= 0 && j < args.IW) {
|
||||
lmax = max(lmax, src[src_off + j]);
|
||||
}
|
||||
float v = src[src_off + j];
|
||||
acc = max(acc, v);
|
||||
}
|
||||
|
||||
dst[dst_off + ow] = acc;
|
||||
float max_val = simd_max(lmax);
|
||||
|
||||
threadgroup float * shared_max = (threadgroup float *) shmem;
|
||||
|
||||
if (ntg > N_SIMDWIDTH) {
|
||||
if (sgitg == 0) {
|
||||
shared_max[tiisg] = -INFINITY;
|
||||
}
|
||||
threadgroup_barrier(mem_flags::mem_threadgroup);
|
||||
|
||||
if (tiisg == 0) {
|
||||
shared_max[sgitg] = max_val;
|
||||
}
|
||||
threadgroup_barrier(mem_flags::mem_threadgroup);
|
||||
|
||||
max_val = shared_max[tiisg];
|
||||
max_val = simd_max(max_val);
|
||||
}
|
||||
|
||||
if (tpitg == 0) {
|
||||
dst[dst_off + ow] = max_val;
|
||||
}
|
||||
}
|
||||
|
||||
kernel void kernel_pool_1d_avg_f32(
|
||||
constant ggml_metal_kargs_pool_1d & args,
|
||||
device const float * src,
|
||||
device float * dst,
|
||||
uint gid [[thread_position_in_grid]]
|
||||
) {
|
||||
|
||||
if (gid >= args.np) {
|
||||
threadgroup char * shmem [[threadgroup(0)]],
|
||||
uint tgpig[[threadgroup_position_in_grid]],
|
||||
uint tpitg[[thread_position_in_threadgroup]],
|
||||
uint sgitg[[simdgroup_index_in_threadgroup]],
|
||||
uint tiisg[[thread_index_in_simdgroup]],
|
||||
uint ntg[[threads_per_threadgroup]]) {
|
||||
if (tgpig >= args.np) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int ow = (int)gid % args.OW;
|
||||
const int row = (int)gid / args.OW;
|
||||
const int out = (int) tgpig;
|
||||
const int ow = out % (int) args.OW;
|
||||
const int row = out / (int) args.OW;
|
||||
|
||||
const int base = ow * args.s0 - args.p0;
|
||||
const int base = ow * args.s0 - args.p0;
|
||||
const int src_off = row * args.IW;
|
||||
const int dst_off = row * args.OW;
|
||||
|
||||
float acc = 0.0f;
|
||||
int cnt = 0;
|
||||
float lsum = 0.0f;
|
||||
int lcnt = 0;
|
||||
|
||||
const int src_off = row * args.IW;
|
||||
const int dst_off = row * args.OW;
|
||||
|
||||
for (int ki = 0; ki < args.k0; ++ki) {
|
||||
for (int ki = tpitg; ki < args.k0; ki += ntg) {
|
||||
const int j = base + ki;
|
||||
if (j < 0 || j >= args.IW) {
|
||||
continue;
|
||||
if (j >= 0 && j < args.IW) {
|
||||
lsum += src[src_off + j];
|
||||
lcnt += 1;
|
||||
}
|
||||
acc += src[src_off + j];
|
||||
cnt += 1;
|
||||
}
|
||||
|
||||
dst[dst_off + ow] = (cnt > 0) ? (acc / (float)cnt) : 0.0f;
|
||||
float sum = simd_sum(lsum);
|
||||
float cnt = simd_sum((float) lcnt);
|
||||
|
||||
threadgroup float * shared_sum = (threadgroup float *) shmem;
|
||||
threadgroup float * shared_cnt = (threadgroup float *) shmem + N_SIMDWIDTH;
|
||||
|
||||
if (ntg > N_SIMDWIDTH) {
|
||||
if (sgitg == 0) {
|
||||
shared_sum[tiisg] = 0.0f;
|
||||
shared_cnt[tiisg] = 0.0f;
|
||||
}
|
||||
threadgroup_barrier(mem_flags::mem_threadgroup);
|
||||
|
||||
if (tiisg == 0) {
|
||||
shared_sum[sgitg] = sum;
|
||||
shared_cnt[sgitg] = cnt;
|
||||
}
|
||||
threadgroup_barrier(mem_flags::mem_threadgroup);
|
||||
|
||||
sum = shared_sum[tiisg];
|
||||
cnt = shared_cnt[tiisg];
|
||||
|
||||
sum = simd_sum(sum);
|
||||
cnt = simd_sum(cnt);
|
||||
}
|
||||
|
||||
if (tpitg == 0) {
|
||||
dst[dst_off + ow] = (cnt > 0.0f) ? (sum / cnt) : 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -831,6 +831,7 @@ struct ggml_backend_opencl_context {
|
||||
cl_program program_mul_mv_id_mxfp4_f32_flat;
|
||||
cl_program program_mul_mm_f32_f32_l4_lm;
|
||||
cl_program program_mul_mm_f16_f32_l4_lm;
|
||||
cl_program program_mul_mm_f16_f32_l4_lm_n8;
|
||||
cl_program program_mul_mm_q8_0_f32_l4_lm;
|
||||
|
||||
cl_kernel kernel_add, kernel_add_row, kernel_add_f16, kernel_add_row_f16;
|
||||
@@ -1052,6 +1053,7 @@ struct ggml_backend_opencl_context {
|
||||
cl_kernel kernel_mul_mm_f32_f32_l4_lm;
|
||||
cl_kernel kernel_gemv_f32_f32_mc; // multi-column (small-N) f32 GEMV for spec/MTP verify
|
||||
cl_kernel kernel_mul_mm_f16_f32_l4_lm;
|
||||
cl_kernel kernel_mul_mm_f16_f32_l4_lm_n8;
|
||||
cl_kernel kernel_mul_mm_q1_0_f32_l4_lm;
|
||||
cl_kernel kernel_mul_mm_q4_0_f32_l4_lm;
|
||||
cl_kernel kernel_mul_mm_q4_1_f32_l4_lm;
|
||||
@@ -1247,6 +1249,8 @@ struct ggml_backend_opencl_context {
|
||||
cl_kernel kernel_gemv_noshuffle_q8_0_f32_splitk; // split-K across WGs (small-M decode)
|
||||
cl_kernel kernel_gemm_noshuffle_q1_0_f32;
|
||||
cl_kernel kernel_gemv_noshuffle_q1_0_f32;
|
||||
cl_kernel kernel_gemv_noshuffle_q1_0_f32_nsg1 = nullptr;
|
||||
int min_m_gemv_nsg1_q1_0 = 0;
|
||||
cl_kernel kernel_gemv_noshuffle_q4_k_f32;
|
||||
cl_kernel kernel_gemv_noshuffle_q4_k_f32_o4; // 4-output-per-WI, long-vocab lm_head
|
||||
cl_kernel kernel_gemv_noshuffle_q4_k_f32_tiled; // tiled-wide layout (opt-in)
|
||||
@@ -1261,6 +1265,11 @@ struct ggml_backend_opencl_context {
|
||||
cl_kernel kernel_gemv_noshuffle_q4_k_f32_32b_trans;
|
||||
cl_kernel kernel_gemm_noshuffle_q4_k_q8_1_dp4a = nullptr; // dp4a (int8) dense prefill GEMM
|
||||
cl_kernel kernel_gemm_noshuffle_q4_k_q8_1_dp4a_wimg = nullptr; // dp4a dense prefill GEMM, weights via texture (X1 opt-in)
|
||||
cl_kernel kernel_gemm_noshuffle_q4_k_q8_1_dp4a_narrow = nullptr;
|
||||
cl_kernel kernel_gemm_noshuffle_q4_k_q8_1_dp4a_narrow_wimg = nullptr;
|
||||
int tile_n_dp4a_q4_k = 32;
|
||||
int tile_n_dp4a_narrow_q4_k = 32;
|
||||
int max_n_dp4a_narrow_q4_k = 16;
|
||||
cl_kernel kernel_gemm_noshuffle_q5_k_q8_1_dp4a = nullptr; // dp4a (int8) dense q5_K prefill GEMM
|
||||
cl_kernel kernel_gemm_noshuffle_q6_k_q8_1_dp4a = nullptr; // dp4a (int8) dense q6_K prefill GEMM
|
||||
cl_kernel kernel_quant_a_q8_1; // plain activation q8_1 pre-pass
|
||||
@@ -2781,6 +2790,11 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
|
||||
build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts);
|
||||
|
||||
CL_CHECK((backend_ctx->kernel_mul_mm_f16_f32_l4_lm = clCreateKernel(backend_ctx->program_mul_mm_f16_f32_l4_lm, "kernel_mul_mm_f16_f32_l4_lm", &err), err));
|
||||
|
||||
const std::string narrow_opts = compile_opts + " -DBN=8 -DTN=1 -DKERNEL_NAME_LM=kernel_mul_mm_f16_f32_l4_lm_n8";
|
||||
backend_ctx->program_mul_mm_f16_f32_l4_lm_n8 =
|
||||
build_program_from_source(backend_ctx, kernel_src.c_str(), narrow_opts);
|
||||
CL_CHECK((backend_ctx->kernel_mul_mm_f16_f32_l4_lm_n8 = clCreateKernel(backend_ctx->program_mul_mm_f16_f32_l4_lm_n8, "kernel_mul_mm_f16_f32_l4_lm_n8", &err), err));
|
||||
GGML_LOG_CONT(".");
|
||||
}
|
||||
|
||||
@@ -3983,6 +3997,16 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
|
||||
|
||||
CL_CHECK((backend_ctx->kernel_gemv_noshuffle_q1_0_f32 = clCreateKernel(prog, "kernel_gemv_noshuffle_q1_0_f32", &err), err));
|
||||
CL_CHECK(clReleaseProgram(prog));
|
||||
|
||||
const char * nsg1_min_m_env = getenv("GGML_OPENCL_Q1_0_GEMV_NSG1_MIN_M");
|
||||
backend_ctx->min_m_gemv_nsg1_q1_0 = nsg1_min_m_env ? atoi(nsg1_min_m_env) :
|
||||
(backend_ctx->adreno_gen == ADRENO_GPU_GEN::X2E ? 8192 : 0);
|
||||
if (backend_ctx->min_m_gemv_nsg1_q1_0 > 0) {
|
||||
const std::string nsg1_opts = CL_gemv_compile_opts + " -DN_SIMDGROUP=1";
|
||||
cl_program prog_nsg1 = build_program_from_source(backend_ctx, kernel_src_CL_gemv_general.c_str(), nsg1_opts);
|
||||
CL_CHECK((backend_ctx->kernel_gemv_noshuffle_q1_0_f32_nsg1 = clCreateKernel(prog_nsg1, "kernel_gemv_noshuffle_q1_0_f32", &err), err));
|
||||
CL_CHECK(clReleaseProgram(prog_nsg1));
|
||||
}
|
||||
GGML_LOG_CONT(".");
|
||||
}
|
||||
|
||||
@@ -4453,16 +4477,32 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
|
||||
#else
|
||||
const std::string kernel_src = read_file("gemm_noshuffle_q4_k_q8_1_dp4a.cl");
|
||||
#endif
|
||||
// Per-device dp4a dense tile. The X2-tuned TILESIZE_N=32 over-occupies LDS on
|
||||
// X1 (1152 B/WG -> few resident WGs); TILESIZE_N=8 (288 B) lifts occupancy on
|
||||
// X1, byte-identical. X2E keeps 32. Env override wins.
|
||||
int q4k_dp4a_ts = (backend_ctx->adreno_gen == ADRENO_GPU_GEN::X1E) ? 8 : 32;
|
||||
if (const char * e = getenv("GGML_OPENCL_Q4K_DP4A_TS")) { q4k_dp4a_ts = atoi(e); }
|
||||
std::string dp4a_opts = compile_opts + " -DTILESIZE_N=" + std::to_string(q4k_dp4a_ts);
|
||||
// X1 uses a smaller N tile to reduce local memory and register use.
|
||||
int tile_n = (backend_ctx->adreno_gen == ADRENO_GPU_GEN::X1E) ? 8 : 32;
|
||||
if (const char * e = getenv("GGML_OPENCL_Q4K_DP4A_TS")) { tile_n = atoi(e); }
|
||||
GGML_ASSERT(tile_n >= 4 && tile_n <= 64 && tile_n % 4 == 0);
|
||||
backend_ctx->tile_n_dp4a_q4_k = tile_n;
|
||||
std::string dp4a_opts = compile_opts + " -DTILESIZE_N=" + std::to_string(tile_n);
|
||||
cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), dp4a_opts);
|
||||
CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q4_k_q8_1_dp4a = clCreateKernel(prog, "kernel_gemm_noshuffle_q4_k_q8_1_dp4a", &err), err));
|
||||
CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q4_k_q8_1_dp4a_wimg = clCreateKernel(prog, "kernel_gemm_noshuffle_q4_k_q8_1_dp4a_wimg", &err), err));
|
||||
CL_CHECK(clReleaseProgram(prog));
|
||||
|
||||
// Compile a smaller N tile to avoid unused columns in small batches.
|
||||
int tile_n_narrow = MIN(tile_n, 16);
|
||||
if (const char * e = getenv("GGML_OPENCL_Q4_K_DP4A_TS_NARROW")) { tile_n_narrow = atoi(e); }
|
||||
GGML_ASSERT(tile_n_narrow >= 4 && tile_n_narrow <= 64 && tile_n_narrow % 4 == 0);
|
||||
if (const char * e = getenv("GGML_OPENCL_Q4_K_DP4A_NARROW_MAX")) {
|
||||
backend_ctx->max_n_dp4a_narrow_q4_k = atoi(e);
|
||||
}
|
||||
backend_ctx->tile_n_dp4a_narrow_q4_k = tile_n_narrow;
|
||||
if (tile_n_narrow != tile_n) {
|
||||
std::string narrow_opts = compile_opts + " -DTILESIZE_N=" + std::to_string(tile_n_narrow);
|
||||
cl_program narrow_prog = build_program_from_source(backend_ctx, kernel_src.c_str(), narrow_opts);
|
||||
CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q4_k_q8_1_dp4a_narrow = clCreateKernel(narrow_prog, "kernel_gemm_noshuffle_q4_k_q8_1_dp4a", &err), err));
|
||||
CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q4_k_q8_1_dp4a_narrow_wimg = clCreateKernel(narrow_prog, "kernel_gemm_noshuffle_q4_k_q8_1_dp4a_wimg", &err), err));
|
||||
CL_CHECK(clReleaseProgram(narrow_prog));
|
||||
}
|
||||
GGML_LOG_CONT(".");
|
||||
}
|
||||
|
||||
@@ -9531,22 +9571,6 @@ static bool ggml_opencl_supports_op(ggml_backend_dev_t dev, const struct ggml_te
|
||||
op->src[0]->ne[1] >= 32768) { // vocab-scale weight; no FFN/attn weight is this tall
|
||||
return false;
|
||||
}
|
||||
// The generic mul_mv (GEMV) kernels are wrong for large-batch prefill on
|
||||
// Adreno. A quant mul_mat only avoids the GEMV when it reaches the Adreno
|
||||
// trans-weight GEMM, which needs both a GEMM kernel for the type and
|
||||
// use_adreno_kernels(). Decline the large-N shapes that would otherwise
|
||||
// fall through to the GEMV.
|
||||
{
|
||||
const ggml_type t = op->src[0]->type;
|
||||
const bool type_has_gemm = (t == GGML_TYPE_Q4_0 || t == GGML_TYPE_Q4_1 ||
|
||||
t == GGML_TYPE_IQ4_NL || t == GGML_TYPE_Q8_0 ||
|
||||
t == GGML_TYPE_Q4_K || t == GGML_TYPE_Q5_K ||
|
||||
t == GGML_TYPE_Q6_K);
|
||||
const bool uses_gemm = type_has_gemm && use_adreno_kernels(backend_ctx, op->src[0]);
|
||||
if (!uses_gemm && op->src[1]->ne[1] >= 512) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return op->src[1]->type == GGML_TYPE_F32 && ggml_is_contiguous(op->src[0]) && ggml_is_contiguous(op->src[1]);
|
||||
} else if (op->src[0]->type == GGML_TYPE_Q8_0) {
|
||||
// ggml_cl_mul_mat_q8_0_f32_adreno now honors src1/dst view_offs (the
|
||||
@@ -19834,7 +19858,9 @@ static void ggml_cl_mul_mat_q1_0_f32_adreno(ggml_backend_t backend, const ggml_t
|
||||
img_desc.buffer = b_sub_buf;
|
||||
CL_CHECK((b_img = clCreateImage(context, CL_MEM_READ_ONLY, &img_fmt, &img_desc, NULL, &err), err));
|
||||
|
||||
kernel = backend_ctx->kernel_gemv_noshuffle_q1_0_f32;
|
||||
// Tall M has enough row parallelism to skip the K split and its reduction.
|
||||
const bool use_nsg1 = backend_ctx->min_m_gemv_nsg1_q1_0 > 0 && M >= backend_ctx->min_m_gemv_nsg1_q1_0;
|
||||
kernel = use_nsg1 ? backend_ctx->kernel_gemv_noshuffle_q1_0_f32_nsg1 : backend_ctx->kernel_gemv_noshuffle_q1_0_f32;
|
||||
|
||||
int r2 = 1;
|
||||
int r3 = 1;
|
||||
@@ -19856,8 +19882,9 @@ static void ggml_cl_mul_mat_q1_0_f32_adreno(ggml_backend_t backend, const ggml_t
|
||||
CL_CHECK(clSetKernelArg(kernel, 14, sizeof(int), &r3));
|
||||
|
||||
size_t wavesize = backend_ctx->adreno_wave_size;
|
||||
size_t local_work_size[] = { wavesize, 4, 1 };
|
||||
size_t global_work_size[] = { CEIL_DIV(M, wavesize)*wavesize, 4, 1 };
|
||||
const size_t nsg = use_nsg1 ? 1 : 4;
|
||||
size_t local_work_size[] = { wavesize, nsg, 1 };
|
||||
size_t global_work_size[] = { CEIL_DIV(M, wavesize)*wavesize, nsg, 1 };
|
||||
|
||||
backend_ctx->enqueue_ndrange_kernel(kernel, 3, global_work_size, local_work_size, dst);
|
||||
|
||||
@@ -20321,7 +20348,14 @@ static void ggml_cl_mul_mat_q4_0_f32_adreno(ggml_backend_t backend, const ggml_t
|
||||
// per-lane K-walk) for small M. Layout stride is fixed (4 uints/block), so only
|
||||
// the K-split count changes; the mc3 kernel reads it via get_local_size(1). The
|
||||
// ne1==1 base kernel hardcodes N_SIMDGROUP=4, so it always stays at 4.
|
||||
const int mc3_nsg = (use_q40_mc3 && ne01 < 4096) ? 8 : 4;
|
||||
int mc3_nsg = (use_q40_mc3 && ne01 < 4096) ? 8 : 4;
|
||||
if (use_q40_mc3) {
|
||||
// Register pressure can lower the kernel limit below the device limit.
|
||||
const size_t max_workgroup_size = backend_ctx->get_kernel_workgroup_size(kernel);
|
||||
while (mc3_nsg > 1 && (size_t)(64 * mc3_nsg) > max_workgroup_size) {
|
||||
mc3_nsg /= 2;
|
||||
}
|
||||
}
|
||||
size_t local_work_size[3] = {64, (size_t)mc3_nsg, 1};
|
||||
size_t global_work_size[3] = {(size_t)CEIL_DIV(ne01/2, 64)*64, (size_t)mc3_nsg, 1};
|
||||
|
||||
@@ -22667,8 +22701,13 @@ static void ggml_cl_mul_mat_q4_k_f32_adreno(ggml_backend_t backend, const ggml_t
|
||||
}
|
||||
}
|
||||
|
||||
cl_kernel dk = use_wimg ? backend_ctx->kernel_gemm_noshuffle_q4_k_q8_1_dp4a_wimg
|
||||
: backend_ctx->kernel_gemm_noshuffle_q4_k_q8_1_dp4a;
|
||||
const bool use_narrow = backend_ctx->kernel_gemm_noshuffle_q4_k_q8_1_dp4a_narrow != nullptr
|
||||
&& N <= backend_ctx->max_n_dp4a_narrow_q4_k;
|
||||
cl_kernel dk = use_narrow
|
||||
? (use_wimg ? backend_ctx->kernel_gemm_noshuffle_q4_k_q8_1_dp4a_narrow_wimg
|
||||
: backend_ctx->kernel_gemm_noshuffle_q4_k_q8_1_dp4a_narrow)
|
||||
: (use_wimg ? backend_ctx->kernel_gemm_noshuffle_q4_k_q8_1_dp4a_wimg
|
||||
: backend_ctx->kernel_gemm_noshuffle_q4_k_q8_1_dp4a);
|
||||
int ai = 0;
|
||||
if (use_wimg) {
|
||||
CL_CHECK(clSetKernelArg(dk, ai++, sizeof(cl_mem), &q4k_q_img));
|
||||
@@ -22689,12 +22728,9 @@ static void ggml_cl_mul_mat_q4_k_f32_adreno(ggml_backend_t backend, const ggml_t
|
||||
CL_CHECK(clSetKernelArg(dk, ai++, sizeof(cl_uchar), &mask_d6));
|
||||
CL_CHECK(clSetKernelArg(dk, ai++, sizeof(cl_uchar), &mask_d4));
|
||||
CL_CHECK(clSetKernelArg(dk, ai++, sizeof(cl_uchar), &mask_hi2));
|
||||
// Must match the compile-time TILESIZE_N chosen at program build (per-device,
|
||||
// X1E=8 else 32; env override). Same inputs -> same value.
|
||||
int q4k_dp4a_ts = (backend_ctx->adreno_gen == ADRENO_GPU_GEN::X1E) ? 8 : 32;
|
||||
if (const char * e = getenv("GGML_OPENCL_Q4K_DP4A_TS")) q4k_dp4a_ts = atoi(e);
|
||||
const int tile_n = use_narrow ? backend_ctx->tile_n_dp4a_narrow_q4_k : backend_ctx->tile_n_dp4a_q4_k;
|
||||
size_t d_local[3] = { 64, 1, 1 };
|
||||
size_t d_global[3] = { 64, (size_t)(M / 64), (size_t)CEIL_DIV(N, q4k_dp4a_ts) };
|
||||
size_t d_global[3] = { 64, (size_t)(M / 64), (size_t)CEIL_DIV(N, tile_n) };
|
||||
backend_ctx->enqueue_ndrange_kernel(dk, 3, d_global, d_local, dst);
|
||||
|
||||
if (q4k_q_img != nullptr) {
|
||||
@@ -24815,7 +24851,9 @@ static void ggml_cl_mul_mat(ggml_backend_t backend, const ggml_tensor * src0, co
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
kernel = backend_ctx->kernel_mul_mm_f16_f32_l4_lm;
|
||||
static const char * mm_narrow_n_env = getenv("GGML_OPENCL_MM_NARROW_N");
|
||||
const bool use_narrow_n = (mm_narrow_n_env == nullptr || mm_narrow_n_env[0] != '0') && ne11 >= 2 && ne11 <= 8;
|
||||
kernel = use_narrow_n ? backend_ctx->kernel_mul_mm_f16_f32_l4_lm_n8 : backend_ctx->kernel_mul_mm_f16_f32_l4_lm;
|
||||
nth0 = 128; // calculated as (BM*BN)/(TM*TN)
|
||||
|
||||
int batch_stride_a = ne00*ne01;
|
||||
@@ -24874,8 +24912,9 @@ static void ggml_cl_mul_mat(ggml_backend_t backend, const ggml_tensor * src0, co
|
||||
CL_CHECK(clSetKernelArg(kernel, 17, sizeof(int), &r2));
|
||||
CL_CHECK(clSetKernelArg(kernel, 18, sizeof(int), &r3));
|
||||
|
||||
// 64 is block tile size BM and BN - change here when BM and BN in the kernel are changed.
|
||||
size_t global_work_size[] = {(size_t)(CEIL_DIV(ne01, 64)*nth0), (size_t)(CEIL_DIV(ne11, 64)), (size_t)ne12*ne13};
|
||||
// BN=8/TN=1 keeps 128 threads for small batches, including attention KQ/KQV.
|
||||
const int bn = use_narrow_n ? 8 : 64;
|
||||
size_t global_work_size[] = {(size_t)(CEIL_DIV(ne01, 64)*nth0), (size_t)(CEIL_DIV(ne11, bn)), (size_t)ne12*ne13};
|
||||
size_t local_work_size[] = {(size_t)nth0, 1, 1};
|
||||
|
||||
backend_ctx->enqueue_ndrange_kernel(kernel, 3, global_work_size, local_work_size, dst);
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
#endif
|
||||
|
||||
#define QK1_0 128
|
||||
#ifndef N_SIMDGROUP
|
||||
#define N_SIMDGROUP 4
|
||||
#endif
|
||||
|
||||
#define dequantizeBlockAccum_q1(total, bits, scale, regB, lb) \
|
||||
total += (2.0f*(float)((bits >> 0) & 1u) - 1.0f) * scale * sub_group_broadcast(regB.s0, lb+0); \
|
||||
@@ -106,15 +108,19 @@ __kernel void kernel_gemv_noshuffle_q1_0_f32(
|
||||
dequantizeBlockAccum_q1(totalSum, regA.s3, scale, regB, 12);
|
||||
}
|
||||
|
||||
// reduction in local memory, assumes #wave = N_SIMDGROUP = 4
|
||||
local float reduceLM[SIMDGROUP_WIDTH * 3];
|
||||
if (groupId == 1) reduceLM[SIMDGROUP_WIDTH * 0 + slid] = totalSum;
|
||||
if (groupId == 2) reduceLM[SIMDGROUP_WIDTH * 1 + slid] = totalSum;
|
||||
if (groupId == 3) reduceLM[SIMDGROUP_WIDTH * 2 + slid] = totalSum;
|
||||
#if N_SIMDGROUP > 1
|
||||
// Sum the K slices from the other subgroups.
|
||||
local float reduceLM[SIMDGROUP_WIDTH * (N_SIMDGROUP - 1)];
|
||||
if (groupId > 0) {
|
||||
reduceLM[SIMDGROUP_WIDTH * (groupId - 1) + slid] = totalSum;
|
||||
}
|
||||
barrier(CLK_LOCAL_MEM_FENCE);
|
||||
if (groupId == 0) totalSum += reduceLM[SIMDGROUP_WIDTH * 0 + slid];
|
||||
if (groupId == 0) totalSum += reduceLM[SIMDGROUP_WIDTH * 1 + slid];
|
||||
if (groupId == 0) totalSum += reduceLM[SIMDGROUP_WIDTH * 2 + slid];
|
||||
if (groupId == 0) {
|
||||
for (uint w = 0; w < N_SIMDGROUP - 1; ++w) {
|
||||
totalSum += reduceLM[SIMDGROUP_WIDTH * w + slid];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (groupId == 0) {
|
||||
dst = (global float*)((global char*)dst + offsetd);
|
||||
@@ -122,6 +128,8 @@ __kernel void kernel_gemv_noshuffle_q1_0_f32(
|
||||
// so when ne01 is not a multiple of the wave size the tail work-items run past
|
||||
// row ne01 and would overrun dst into the adjacent tensor. No-op / byte-identical
|
||||
// when ne01 is wave-aligned (no padding).
|
||||
if (gid < M) dst[gid] = totalSum;
|
||||
if (gid < M) {
|
||||
dst[gid] = totalSum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,20 @@
|
||||
#define LOAD_VEC_B 4
|
||||
|
||||
#define BM 64
|
||||
#ifndef BN
|
||||
#define BN 64
|
||||
#endif
|
||||
#define BK 16
|
||||
#define TM 4
|
||||
#ifndef TN
|
||||
#define TN 8
|
||||
#endif
|
||||
|
||||
kernel void kernel_mul_mm_f16_f32_l4_lm(
|
||||
#ifndef KERNEL_NAME_LM
|
||||
#define KERNEL_NAME_LM kernel_mul_mm_f16_f32_l4_lm
|
||||
#endif
|
||||
|
||||
kernel void KERNEL_NAME_LM(
|
||||
global half4 * src0,
|
||||
ulong offset0,
|
||||
global float4 * src1,
|
||||
@@ -93,7 +101,8 @@ kernel void kernel_mul_mm_f16_f32_l4_lm(
|
||||
}
|
||||
}
|
||||
|
||||
for (int l = 0; l < BN; l += loadstride_b) {
|
||||
// BN=8 uses only the threads that load a column inside the tile.
|
||||
for (int l = 0; loadc_b + l < BN; l += loadstride_b) {
|
||||
if (ic*BN + loadc_b + l < ne11) {
|
||||
const int idx = pos_b + (loadc_b + l) * stride_b / LOAD_VEC_B + loadr_b;
|
||||
buf_b[(loadr_b * LOAD_VEC_B + 0) * BN + loadc_b + l] = src1[idx].s0;
|
||||
|
||||
@@ -132,7 +132,7 @@ kernel void kernel_mul_mv_mxfp4_f32_flat(
|
||||
global float4 * y4 = (global float4 *)yb;
|
||||
|
||||
#pragma unroll
|
||||
for (short row = 0; row < N_R0_MXFP4; row++) {
|
||||
for (short row = 0; row < N_R0_MXFP4 && first_row + row < ne0; row++) {
|
||||
uchar xb_e = x_e[row * nb + ib];
|
||||
#ifdef SRC0Q_IMG
|
||||
ushort4 xb_q = as_ushort4(read_imageui(src0_q, (offset_q + row * nb + ib) * 2 + it).xy);
|
||||
|
||||
@@ -520,7 +520,7 @@ kernel void kernel_rope_multi_f16(
|
||||
src0 = (global void*)((global char*)src0 + offset0);
|
||||
src1 = (global int*)((global char*)src1 + offset1);
|
||||
src2 = (global float*)((global char*)src2 + offset2);
|
||||
dst = (global float*)((global char*)dst + offsetd);
|
||||
dst = (global half*)((global char*)dst + offsetd);
|
||||
|
||||
int i3 = get_group_id(2);
|
||||
int i2 = get_group_id(1);
|
||||
@@ -714,7 +714,7 @@ kernel void kernel_rope_vision_f16(
|
||||
src0 = (global void*)((global char*)src0 + offset0);
|
||||
src1 = (global int*)((global char*)src1 + offset1);
|
||||
src2 = (global float*)((global char*)src2 + offset2);
|
||||
dst = (global float*)((global char*)dst + offsetd);
|
||||
dst = (global half*)((global char*)dst + offsetd);
|
||||
|
||||
int i3 = get_group_id(2);
|
||||
int i2 = get_group_id(1);
|
||||
|
||||
@@ -202,7 +202,8 @@ bool ggml_sycl_flash_attn_ext_sparse(ggml_backend_sycl_context & ctx, ggml_tenso
|
||||
uint8_t * d_V = v_alloc.get();
|
||||
sycl::half * d_mask = m_alloc.get();
|
||||
|
||||
SYCL_CHECK(CHECK_TRY_ERROR(stream->memset(d_cnt, 0, sizeof(int32_t))));
|
||||
// reset with a kernel: on Level Zero a queue memset is not always ordered before the next kernel
|
||||
stream->single_task([=]() { *d_cnt = 0; });
|
||||
|
||||
sparse_fa_compact_mask(stream, (const sycl::half *) mask->data,
|
||||
d_idx, d_cnt, n_kv, n_kv_g);
|
||||
|
||||
@@ -344,6 +344,7 @@ class Keys:
|
||||
ROUTING_BLOCK_COUNT = "{arch}.decision.routing_block_count"
|
||||
HEAD_COUNT = "{arch}.decision.head_count"
|
||||
MAX_HEAD_TOKENS = "{arch}.decision.max_head_tokens"
|
||||
LABELS = "{arch}.decision.labels" # the text of the token read for each option, in order
|
||||
TEMPERATURE = "{arch}.decision.temperature.{name}" # name: "<type>" or "<type>.<n_opt bucket>"
|
||||
SCORE_BIAS = "{arch}.decision.score_bias.{n}.{i}" # added to the logit of level i of a score question with n levels
|
||||
|
||||
@@ -419,6 +420,8 @@ class Keys:
|
||||
PREPROC_MIN_TILES = "clip.vision.preproc_min_tiles"
|
||||
PREPROC_MAX_TILES = "clip.vision.preproc_max_tiles"
|
||||
PREPROC_IMAGE_SIZE = "clip.vision.preproc_image_size"
|
||||
PREPROC_TILE_SIZE = "clip.vision.preproc_tile_size"
|
||||
PREPROC_MAX_PIXELS_TOLERANCE = "clip.vision.preproc_max_pixels_tolerance"
|
||||
PATCH_SIZE = "clip.vision.patch_size"
|
||||
EMBEDDING_LENGTH = "clip.vision.embedding_length"
|
||||
FEED_FORWARD_LENGTH = "clip.vision.feed_forward_length"
|
||||
@@ -6337,6 +6340,7 @@ class DecisionType:
|
||||
LFM2_D1 = "lfm2-d1" # same as openjev, the labels depend on the question type
|
||||
LFM2_D1_OMNI = "lfm2-d1-omni" # same head as laya on a bidirectional LFM2 trunk, other prompt layout
|
||||
GRANITE_SWITCH = "granite-switch" # same as openjev, run by the decision aLoRA of granite switch
|
||||
H2O_LIGHTNING = "h2o-lightning" # same as openjev, labels read after an answer prefill
|
||||
|
||||
|
||||
class VisionProjectorType:
|
||||
|
||||
@@ -1401,6 +1401,9 @@ class GGUFWriter:
|
||||
def add_decision_max_head_tokens(self, value: int) -> None:
|
||||
self.add_uint32(Keys.Decision.MAX_HEAD_TOKENS.format(arch=self.arch), value)
|
||||
|
||||
def add_decision_labels(self, labels: Sequence[str]) -> None:
|
||||
self.add_array(Keys.Decision.LABELS.format(arch=self.arch), labels)
|
||||
|
||||
def add_decision_temperature(self, name: str, value: float) -> None:
|
||||
self.add_float32(Keys.Decision.TEMPERATURE.format(arch=self.arch, name=name), value)
|
||||
|
||||
@@ -1470,6 +1473,12 @@ class GGUFWriter:
|
||||
def add_vision_preproc_image_size(self, value: int) -> None:
|
||||
self.add_uint32(Keys.ClipVision.PREPROC_IMAGE_SIZE, value)
|
||||
|
||||
def add_vision_preproc_tile_size(self, value: int) -> None:
|
||||
self.add_uint32(Keys.ClipVision.PREPROC_TILE_SIZE, value)
|
||||
|
||||
def add_vision_preproc_max_pixels_tolerance(self, value: float) -> None:
|
||||
self.add_float32(Keys.ClipVision.PREPROC_MAX_PIXELS_TOLERANCE, value)
|
||||
|
||||
def add_vision_projector_query_side(self, value: int) -> None:
|
||||
self.add_uint32(Keys.ClipVision.Projector.QUERY_SIDE, value)
|
||||
|
||||
|
||||
@@ -383,6 +383,7 @@ static const std::map<llm_kv, const char *> LLM_KV_NAMES = {
|
||||
{ LLM_KV_DECISION_BLOCK_COUNT, "%s.decision.block_count" },
|
||||
{ LLM_KV_DECISION_ROUTING_BLOCK_COUNT, "%s.decision.routing_block_count" },
|
||||
{ LLM_KV_DECISION_HEAD_COUNT, "%s.decision.head_count" },
|
||||
{ LLM_KV_DECISION_LABELS, "%s.decision.labels" },
|
||||
|
||||
{ LLM_KV_TARGET_LAYERS, "%s.target_layers" },
|
||||
{ LLM_KV_TARGET_HIDDEN_SIZE, "%s.target_hidden_size" },
|
||||
|
||||
@@ -431,6 +431,7 @@ enum llm_kv {
|
||||
LLM_KV_DECISION_BLOCK_COUNT,
|
||||
LLM_KV_DECISION_ROUTING_BLOCK_COUNT,
|
||||
LLM_KV_DECISION_HEAD_COUNT,
|
||||
LLM_KV_DECISION_LABELS,
|
||||
|
||||
LLM_KV_TARGET_LAYERS,
|
||||
LLM_KV_TARGET_HIDDEN_SIZE,
|
||||
|
||||
@@ -122,6 +122,9 @@ enum llama_decision_order {
|
||||
// The embeddings output has one value per entry: row i is the score of option i
|
||||
LLAMA_API bool llama_batch_ext_set_decision_order(struct llama_batch_ext * batch, int32_t idx, enum llama_decision_order order);
|
||||
|
||||
// Label of option i of a decision model ("<arch>.decision.labels"), nullptr past the last one
|
||||
LLAMA_API const char * llama_model_decision_label(const struct llama_model * model, uint32_t i);
|
||||
|
||||
// mirrors:
|
||||
// LLAMA_API float * llama_get_embeddings(struct llama_context * ctx);
|
||||
LLAMA_API float * llama_get_embeddings_nextn(struct llama_context * ctx);
|
||||
|
||||
@@ -1549,6 +1549,9 @@ void llama_model_base::load_hparams(llama_model_loader & ml) {
|
||||
hparams.n_cls_out = classifier_labels.size();
|
||||
}
|
||||
|
||||
// for decision models
|
||||
ml.get_arr(LLM_KV_DECISION_LABELS, decision_labels, false);
|
||||
|
||||
// per-arch hparams
|
||||
load_arch_hparams(ml);
|
||||
|
||||
@@ -3438,6 +3441,14 @@ const char * llama_model_cls_label(const struct llama_model * model, uint32_t i)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char * llama_model_decision_label(const struct llama_model * model, uint32_t i) {
|
||||
if (i < model->decision_labels.size()) {
|
||||
return model->decision_labels[i].c_str();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// deprecated
|
||||
int32_t llama_n_ctx_train(const llama_model * model) {
|
||||
return llama_model_n_ctx_train(model);
|
||||
|
||||
@@ -724,6 +724,7 @@ struct llama_model {
|
||||
|
||||
// for classifier models
|
||||
std::vector<std::string> classifier_labels;
|
||||
std::vector<std::string> decision_labels;
|
||||
|
||||
struct ggml_tensor * tok_embd = nullptr;
|
||||
struct ggml_tensor * type_embd = nullptr;
|
||||
|
||||
@@ -131,11 +131,8 @@ llama_model_kolibri1::graph::graph(const llama_model & model, const llm_graph_pa
|
||||
cur = build_norm(ffn_inp, layer.ffn_norm, NULL, LLM_NORM_RMS, il);
|
||||
cb(cur, "ffn_norm", il);
|
||||
|
||||
// The router runs in f32: the selection adds the bias to the logits, the weights
|
||||
// are the sigmoid of the unbiased logits, so the same logits feed both.
|
||||
// The selection adds the bias to the logits, the weights are the sigmoid of the unbiased logits, so the same logits feed both.
|
||||
ggml_tensor * logits = build_lora_mm(layer.ffn_gate_inp, cur);
|
||||
ggml_prec_set_acc(logits, GGML_PREC_F32);
|
||||
ggml_prec_set_src(logits, GGML_PREC_F32, 1);
|
||||
cb(logits, "ffn_moe_logits", il);
|
||||
|
||||
ggml_tensor * selection = ggml_add(ctx0, logits, layer.ffn_exp_probs_b);
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
#include "models.h"
|
||||
|
||||
#include "llama-memory-recurrent.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
llm_build_lfm_base::llm_build_lfm_base(const llm_graph_params & params) : llm_graph_context(params) {}
|
||||
|
||||
ggml_tensor * llm_build_lfm_base::build_lfm_shortconv(const llama_model & model,
|
||||
const llama_memory_recurrent_context * mctx_recr,
|
||||
llm_graph_input_rs * inp_recr,
|
||||
ggml_tensor * cur,
|
||||
int il) {
|
||||
const uint32_t kv_head = mctx_recr->get_head();
|
||||
const int64_t n_seq_tokens = ubatch.n_seq_tokens;
|
||||
const int64_t n_seqs = ubatch.n_seqs;
|
||||
GGML_ASSERT(n_seqs != 0);
|
||||
GGML_ASSERT(ubatch.equal_seqs());
|
||||
GGML_ASSERT(ubatch.n_tokens == n_seq_tokens * n_seqs);
|
||||
|
||||
GGML_ASSERT(hparams.n_shortconv_l_cache > 1);
|
||||
const uint32_t d_conv = hparams.n_shortconv_l_cache - 1;
|
||||
const int64_t n_embd = hparams.n_embd;
|
||||
|
||||
// {n_embd, n_tokens} => {n_embd, n_seq_tokens, n_seqs}
|
||||
cur = ggml_reshape_3d(ctx0, cur, cur->ne[0], n_seq_tokens, n_seqs);
|
||||
|
||||
auto * bcx = build_lora_mm(model.layers[il].shortconv.in_proj, cur);
|
||||
cb(bcx, "model.layers.{}.conv.in_proj", il);
|
||||
|
||||
constexpr auto n_chunks = 3;
|
||||
GGML_ASSERT(bcx->ne[0] % n_chunks == 0);
|
||||
const auto chunk_size = bcx->ne[0] / n_chunks;
|
||||
auto * b = ggml_view_3d(ctx0, bcx, chunk_size, bcx->ne[1], bcx->ne[2], bcx->nb[1], bcx->nb[2],
|
||||
0 * chunk_size * ggml_element_size(bcx));
|
||||
auto * c = ggml_view_3d(ctx0, bcx, chunk_size, bcx->ne[1], bcx->ne[2], bcx->nb[1], bcx->nb[2],
|
||||
1 * chunk_size * ggml_element_size(bcx));
|
||||
auto * x = ggml_view_3d(ctx0, bcx, chunk_size, bcx->ne[1], bcx->ne[2], bcx->nb[1], bcx->nb[2],
|
||||
2 * chunk_size * ggml_element_size(bcx));
|
||||
|
||||
auto * bx = ggml_transpose(ctx0, ggml_mul(ctx0, b, x));
|
||||
|
||||
// read conv state
|
||||
auto * conv_state = mctx_recr->get_r_l(il);
|
||||
auto * conv_rs = build_rs(inp_recr, conv_state, hparams.n_embd_r(), n_seqs);
|
||||
auto * conv = ggml_reshape_3d(ctx0, conv_rs, d_conv, n_embd, n_seqs);
|
||||
|
||||
// causal prepends the state, non-causal pads symmetrically for a centered window
|
||||
if (hparams.causal_attn) {
|
||||
bx = ggml_concat(ctx0, conv, bx, 0);
|
||||
} else {
|
||||
const int64_t pad = (hparams.n_shortconv_l_cache - 1) / 2;
|
||||
auto * left = ggml_cont(ctx0,
|
||||
ggml_view_3d(ctx0, conv, pad, n_embd, n_seqs, conv->nb[1], conv->nb[2], (d_conv - pad) * conv->nb[0]));
|
||||
bx = ggml_pad_ext(ctx0, ggml_concat(ctx0, left, bx, 0), 0, pad, 0, 0, 0, 0, 0, 0);
|
||||
}
|
||||
GGML_ASSERT(bx->ne[0] > conv->ne[0]);
|
||||
|
||||
// write conv states: slot 0 = the final state, slot s = the state s tokens back (partial rollback)
|
||||
const int64_t K = hparams.causal_attn && cparams.n_rs_seq > 0 ? (int64_t) cparams.n_rs_seq + 1 : 1;
|
||||
const int64_t n_written = std::min<int64_t>(n_seq_tokens, K);
|
||||
const auto mem_size = mctx_recr->get_size();
|
||||
const size_t row_size = ggml_row_size(conv_state->type, (int64_t) d_conv * n_embd);
|
||||
|
||||
for (int64_t slot = 0; slot < n_written; ++slot) {
|
||||
auto * conv_snap = ggml_view_3d(ctx0, bx, d_conv, bx->ne[1], bx->ne[2], bx->nb[1], bx->nb[2],
|
||||
(bx->ne[0] - d_conv - slot) * ggml_element_size(bx));
|
||||
ggml_build_forward_expand(gf, ggml_cpy(ctx0, conv_snap,
|
||||
ggml_view_2d(ctx0, conv_state, (int64_t) d_conv * n_embd, n_seqs,
|
||||
conv_state->nb[1],
|
||||
((size_t) slot * mem_size + kv_head) * row_size)));
|
||||
}
|
||||
|
||||
auto * conv_kernel = model.layers[il].shortconv.conv;
|
||||
auto * conv_out = ggml_ssm_conv(ctx0, bx, conv_kernel);
|
||||
cb(conv_out, "model.layers.{}.conv.conv", il);
|
||||
|
||||
auto * y = ggml_mul(ctx0, c, conv_out);
|
||||
y = build_lora_mm(model.layers[il].shortconv.out_proj, y);
|
||||
cb(y, "model.layers.{}.conv.out_proj", il);
|
||||
// {n_embd, n_seq_tokens, n_seqs} => {n_embd, n_tokens}
|
||||
y = ggml_reshape_2d(ctx0, y, y->ne[0], n_seq_tokens * n_seqs);
|
||||
|
||||
return y;
|
||||
}
|
||||
|
||||
ggml_tensor * llm_build_lfm_base::build_lfm_dense_ffn(const llama_model & model,
|
||||
ggml_tensor * cur,
|
||||
int il) {
|
||||
GGML_ASSERT(!model.layers[il].ffn_up_b);
|
||||
GGML_ASSERT(!model.layers[il].ffn_gate_b);
|
||||
GGML_ASSERT(!model.layers[il].ffn_down_b);
|
||||
return 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_SILU, LLM_FFN_PAR, il);
|
||||
}
|
||||
|
||||
ggml_tensor * llm_build_lfm_base::build_lfm_moe_ffn(const llama_model & model,
|
||||
ggml_tensor * cur,
|
||||
int il) {
|
||||
return 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_SILU, true,
|
||||
hparams.expert_weights_scale,
|
||||
static_cast<llama_expert_gating_func_type>(hparams.expert_gating_func),
|
||||
il);
|
||||
}
|
||||
+8
-104
@@ -147,35 +147,11 @@ std::unique_ptr<llm_graph_context> llama_model_lfm2::build_arch_graph(const llm_
|
||||
|
||||
template <bool iswa>
|
||||
llama_model_lfm2::graph<iswa>::graph(const llama_model & model, const llm_graph_params & params) :
|
||||
llm_graph_context(params) {
|
||||
llm_build_lfm_base(params) {
|
||||
using inp_hybrid_type = std::conditional_t<iswa, llm_graph_input_mem_hybrid_iswa, llm_graph_input_mem_hybrid>;
|
||||
using inp_attn_type = std::conditional_t<iswa, llm_graph_input_attn_kv_iswa, llm_graph_input_attn_kv>;
|
||||
using mem_hybrid_ctx = std::conditional_t<iswa, llama_memory_hybrid_iswa_context, llama_memory_hybrid_context>;
|
||||
|
||||
// lambda helpers for readability
|
||||
auto build_dense_feed_forward = [&model, this](ggml_tensor * cur, int il) -> ggml_tensor * {
|
||||
GGML_ASSERT(!model.layers[il].ffn_up_b);
|
||||
GGML_ASSERT(!model.layers[il].ffn_gate_b);
|
||||
GGML_ASSERT(!model.layers[il].ffn_down_b);
|
||||
return 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_SILU, LLM_FFN_PAR, il);
|
||||
};
|
||||
auto build_moe_feed_forward = [&model, this](ggml_tensor * cur, int il) -> ggml_tensor * {
|
||||
return 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_SILU, true,
|
||||
hparams.expert_weights_scale,
|
||||
static_cast<llama_expert_gating_func_type>(hparams.expert_gating_func),
|
||||
il);
|
||||
};
|
||||
auto build_attn_block = [&model, this](ggml_tensor * cur,
|
||||
ggml_tensor * inp_pos,
|
||||
inp_attn_type * inp_attn,
|
||||
@@ -207,82 +183,6 @@ llama_model_lfm2::graph<iswa>::graph(const llama_model & model, const llm_graph_
|
||||
|
||||
return cur;
|
||||
};
|
||||
auto build_shortconv_block = [&model, this](ggml_tensor * cur,
|
||||
llm_graph_input_rs * inp_recr,
|
||||
int il) -> ggml_tensor * {
|
||||
const auto * mctx_cur = static_cast<const mem_hybrid_ctx *>(mctx)->get_recr();
|
||||
const uint32_t kv_head = mctx_cur->get_head();
|
||||
const int64_t n_seq_tokens = ubatch.n_seq_tokens;
|
||||
const int64_t n_seqs = ubatch.n_seqs;
|
||||
GGML_ASSERT(n_seqs != 0);
|
||||
GGML_ASSERT(ubatch.equal_seqs());
|
||||
GGML_ASSERT(ubatch.n_tokens == n_seq_tokens * n_seqs);
|
||||
|
||||
GGML_ASSERT(hparams.n_shortconv_l_cache > 1);
|
||||
const uint32_t d_conv = hparams.n_shortconv_l_cache - 1;
|
||||
|
||||
// {n_embd, n_tokens} => {n_embd, n_seq_tokens, n_seqs}
|
||||
cur = ggml_reshape_3d(ctx0, cur, cur->ne[0], n_seq_tokens, n_seqs);
|
||||
|
||||
auto * bcx = build_lora_mm(model.layers[il].shortconv.in_proj, cur);
|
||||
cb(bcx, "model.layers.{}.conv.in_proj", il);
|
||||
|
||||
constexpr auto n_chunks = 3;
|
||||
GGML_ASSERT(bcx->ne[0] % n_chunks == 0);
|
||||
const auto chunk_size = bcx->ne[0] / n_chunks;
|
||||
auto * b = ggml_view_3d(ctx0, bcx, chunk_size, bcx->ne[1], bcx->ne[2], bcx->nb[1], bcx->nb[2],
|
||||
0 * chunk_size * ggml_element_size(bcx));
|
||||
auto * c = ggml_view_3d(ctx0, bcx, chunk_size, bcx->ne[1], bcx->ne[2], bcx->nb[1], bcx->nb[2],
|
||||
1 * chunk_size * ggml_element_size(bcx));
|
||||
auto * x = ggml_view_3d(ctx0, bcx, chunk_size, bcx->ne[1], bcx->ne[2], bcx->nb[1], bcx->nb[2],
|
||||
2 * chunk_size * ggml_element_size(bcx));
|
||||
|
||||
auto * bx = ggml_transpose(ctx0, ggml_mul(ctx0, b, x));
|
||||
|
||||
// read conv state
|
||||
auto * conv_state = mctx_cur->get_r_l(il);
|
||||
auto * conv_rs = build_rs(inp_recr, conv_state, hparams.n_embd_r(), n_seqs);
|
||||
auto * conv = ggml_reshape_3d(ctx0, conv_rs, d_conv, hparams.n_embd, n_seqs);
|
||||
|
||||
// causal prepends the state, non-causal pads symmetrically for a centered window
|
||||
if (hparams.causal_attn) {
|
||||
bx = ggml_concat(ctx0, conv, bx, 0);
|
||||
} else {
|
||||
const int64_t pad = (hparams.n_shortconv_l_cache - 1) / 2;
|
||||
auto * left = ggml_cont(ctx0,
|
||||
ggml_view_3d(ctx0, conv, pad, hparams.n_embd, n_seqs, conv->nb[1], conv->nb[2], (d_conv - pad) * conv->nb[0]));
|
||||
bx = ggml_pad_ext(ctx0, ggml_concat(ctx0, left, bx, 0), 0, pad, 0, 0, 0, 0, 0, 0);
|
||||
}
|
||||
GGML_ASSERT(bx->ne[0] > conv->ne[0]);
|
||||
|
||||
// write conv states: slot 0 = the final state, slot s = the state s tokens back (partial rollback)
|
||||
const int64_t K = hparams.causal_attn && cparams.n_rs_seq > 0 ? (int64_t) cparams.n_rs_seq + 1 : 1;
|
||||
const int64_t n_written = std::min<int64_t>(n_seq_tokens, K);
|
||||
const auto mem_size = mctx_cur->get_size();
|
||||
const size_t row_size = ggml_row_size(conv_state->type, (int64_t) d_conv * n_embd);
|
||||
|
||||
for (int64_t slot = 0; slot < n_written; ++slot) {
|
||||
auto * conv_snap = ggml_view_3d(ctx0, bx, d_conv, bx->ne[1], bx->ne[2], bx->nb[1], bx->nb[2],
|
||||
(bx->ne[0] - d_conv - slot) * ggml_element_size(bx));
|
||||
ggml_build_forward_expand(gf, ggml_cpy(ctx0, conv_snap,
|
||||
ggml_view_2d(ctx0, conv_state, (int64_t) d_conv * n_embd, n_seqs,
|
||||
conv_state->nb[1],
|
||||
((size_t) slot * mem_size + kv_head) * row_size)));
|
||||
}
|
||||
|
||||
auto * conv_kernel = model.layers[il].shortconv.conv;
|
||||
auto * conv_out = ggml_ssm_conv(ctx0, bx, conv_kernel);
|
||||
cb(conv_out, "model.layers.{}.conv.conv", il);
|
||||
|
||||
auto * y = ggml_mul(ctx0, c, conv_out);
|
||||
y = build_lora_mm(model.layers[il].shortconv.out_proj, y);
|
||||
cb(y, "model.layers.{}.conv.out_proj", il);
|
||||
// {n_embd, n_seq_tokens, n_seqs} => {n_embd, n_tokens}
|
||||
y = ggml_reshape_2d(ctx0, y, y->ne[0], n_seq_tokens * n_seqs);
|
||||
|
||||
return y;
|
||||
};
|
||||
|
||||
// actual graph construction starts here
|
||||
ggml_tensor * cur = build_inp_embd(model.tok_embd);
|
||||
cb(cur, "model.embed_tokens", -1);
|
||||
@@ -308,8 +208,12 @@ llama_model_lfm2::graph<iswa>::graph(const llama_model & model, const llm_graph_
|
||||
cur = build_norm(cur, model.layers[il].attn_norm, NULL, LLM_NORM_RMS, il);
|
||||
cb(cur, "model.layers.{}.operator_norm", il);
|
||||
|
||||
cur = hparams.is_recr(il) ? build_shortconv_block(cur, inp_hybrid->get_recr(), il) :
|
||||
build_attn_block(cur, inp_pos, inp_hybrid->get_attn(), il);
|
||||
if (hparams.is_recr(il)) {
|
||||
const auto * mctx_recr = static_cast<const mem_hybrid_ctx *>(mctx)->get_recr();
|
||||
cur = build_lfm_shortconv(model, mctx_recr, inp_hybrid->get_recr(), cur, il);
|
||||
} else {
|
||||
cur = build_attn_block(cur, inp_pos, inp_hybrid->get_attn(), il);
|
||||
}
|
||||
|
||||
if (il == n_layer - 1 && inp_out_ids) {
|
||||
cur = ggml_get_rows(ctx0, cur, inp_out_ids);
|
||||
@@ -322,7 +226,7 @@ llama_model_lfm2::graph<iswa>::graph(const llama_model & model, const llm_graph_
|
||||
cb(ffn_norm_out, "model.layers.{}.ffn_norm", il);
|
||||
|
||||
ggml_tensor * ffn_out =
|
||||
is_moe_layer ? build_moe_feed_forward(ffn_norm_out, il) : build_dense_feed_forward(ffn_norm_out, il);
|
||||
is_moe_layer ? build_lfm_moe_ffn(model, ffn_norm_out, il) : build_lfm_dense_ffn(model, ffn_norm_out, il);
|
||||
cb(ffn_norm_out, "model.layers.{}.ffn_out", il);
|
||||
|
||||
cur = ggml_add(ctx0, cur, ffn_out);
|
||||
|
||||
+21
-1
@@ -100,6 +100,26 @@ struct llm_build_delta_net_base : public llm_graph_context {
|
||||
int il);
|
||||
};
|
||||
|
||||
struct llm_build_lfm_base : public llm_graph_context {
|
||||
llm_build_lfm_base(const llm_graph_params & params);
|
||||
|
||||
virtual ~llm_build_lfm_base() = default;
|
||||
|
||||
ggml_tensor * build_lfm_shortconv(const llama_model & model,
|
||||
const llama_memory_recurrent_context * mctx_recr,
|
||||
llm_graph_input_rs * inp_recr,
|
||||
ggml_tensor * cur,
|
||||
int il);
|
||||
|
||||
ggml_tensor * build_lfm_dense_ffn(const llama_model & model,
|
||||
ggml_tensor * cur,
|
||||
int il);
|
||||
|
||||
ggml_tensor * build_lfm_moe_ffn(const llama_model & model,
|
||||
ggml_tensor * cur,
|
||||
int il);
|
||||
};
|
||||
|
||||
struct llm_build_rwkv6_base : public llm_graph_context {
|
||||
const llama_model & model;
|
||||
|
||||
@@ -2264,7 +2284,7 @@ struct llama_model_lfm2 : public llama_model_base {
|
||||
void load_arch_tensors(llama_model_loader & ml) override;
|
||||
|
||||
template <bool iswa>
|
||||
struct graph : public llm_graph_context {
|
||||
struct graph : public llm_build_lfm_base {
|
||||
graph(const llama_model & model, const llm_graph_params & params);
|
||||
};
|
||||
|
||||
|
||||
@@ -465,17 +465,14 @@ ggml_tensor * llama_model_qwen35::graph::build_layer_attn_linear(
|
||||
// Apply gated normalization: self.norm(core_attn_out, z)
|
||||
ggml_tensor * attn_out_norm = build_norm_gated(output, model.layers[il].ssm_norm, z_2d, il);
|
||||
|
||||
// Final reshape: [head_dim, n_heads, n_tokens, n_seqs] -> [n_tokens, n_seqs, n_heads * head_dim]
|
||||
ggml_tensor * final_output = ggml_reshape_3d(ctx0, attn_out_norm, head_v_dim * num_v_heads, n_seq_tokens, n_seqs);
|
||||
// Final reshape: [head_dim, n_heads, n_tokens, n_seqs] -> [n_heads * head_dim , n_tokens * n_seqs ]
|
||||
ggml_tensor * final_output = ggml_reshape_2d(ctx0, attn_out_norm, head_v_dim * num_v_heads, n_seq_tokens * n_seqs);
|
||||
cb(final_output, "final_output", il);
|
||||
|
||||
// Output projection
|
||||
cur = build_lora_mm(model.layers[il].ssm_out, final_output, model.layers[il].ssm_out_s);
|
||||
cb(cur, "linear_attn_out", il);
|
||||
|
||||
// Reshape back to original dimensions
|
||||
cur = ggml_reshape_2d(ctx0, cur, n_embd, n_seq_tokens * n_seqs);
|
||||
|
||||
return cur;
|
||||
}
|
||||
|
||||
|
||||
@@ -475,17 +475,13 @@ ggml_tensor * llama_model_qwen35moe::graph::build_layer_attn_linear(
|
||||
// Apply gated normalization: self.norm(core_attn_out, z)
|
||||
ggml_tensor * attn_out_norm = build_norm_gated(output, model.layers[il].ssm_norm, z_2d, il);
|
||||
|
||||
// Final reshape: [head_dim, n_heads, n_tokens, n_seqs] -> [n_tokens, n_seqs, n_heads * head_dim]
|
||||
ggml_tensor * final_output = ggml_reshape_3d(ctx0, attn_out_norm, head_v_dim * num_v_heads, n_seq_tokens, n_seqs);
|
||||
cb(final_output, "final_output", il);
|
||||
// Final reshape: [head_dim, n_heads, n_tokens, n_seqs] -> [n_heads * head_dim , n_tokens * n_seqs ]
|
||||
ggml_tensor * final_output = ggml_reshape_2d(ctx0, attn_out_norm, head_v_dim * num_v_heads, n_seq_tokens * n_seqs);
|
||||
|
||||
// Output projection
|
||||
cur = build_lora_mm(model.layers[il].ssm_out, final_output, model.layers[il].ssm_out_s);
|
||||
cb(cur, "linear_attn_out", il);
|
||||
|
||||
// Reshape back to original dimensions
|
||||
cur = ggml_reshape_2d(ctx0, cur, n_embd, n_seq_tokens * n_seqs);
|
||||
|
||||
return cur;
|
||||
}
|
||||
|
||||
|
||||
@@ -542,17 +542,14 @@ ggml_tensor * llama_model_qwen3next::graph::build_layer_attn_linear(
|
||||
// Apply gated normalization: self.norm(core_attn_out, z)
|
||||
ggml_tensor * attn_out_norm = build_norm_gated(output, model.layers[il].ssm_norm, z_2d, il);
|
||||
|
||||
// Final reshape: [head_dim, n_heads, n_tokens, n_seqs] -> [n_tokens, n_seqs, n_heads * head_dim]
|
||||
ggml_tensor * final_output = ggml_reshape_3d(ctx0, attn_out_norm, head_v_dim * num_v_heads, n_seq_tokens, n_seqs);
|
||||
// Final reshape: [head_dim, n_heads, n_tokens, n_seqs] -> [n_heads * head_dim , n_tokens * n_seqs ]
|
||||
ggml_tensor * final_output = ggml_reshape_2d(ctx0, attn_out_norm, head_v_dim * num_v_heads, n_seq_tokens * n_seqs);
|
||||
cb(final_output, "final_output", il);
|
||||
|
||||
// Output projection
|
||||
cur = build_lora_mm(model.layers[il].ssm_out, final_output);
|
||||
cb(cur, "linear_attn_out", il);
|
||||
|
||||
// Reshape back to original dimensions
|
||||
cur = ggml_reshape_2d(ctx0, cur, n_embd, n_seq_tokens * n_seqs);
|
||||
|
||||
return cur;
|
||||
}
|
||||
|
||||
|
||||
@@ -1131,14 +1131,12 @@ ggml_tensor * llama_model_qwen4exp::graph::build_layer_attn_linear(
|
||||
// gated normalization, as self.norm(core_attn_out, z) in the reference
|
||||
ggml_tensor * attn_out_norm = build_norm_gated(output, model.layers[il].ssm_norm, z_2d, il);
|
||||
|
||||
ggml_tensor * final_output = ggml_reshape_3d(ctx0, attn_out_norm, head_v_dim * num_v_heads, n_seq_tokens, n_seqs);
|
||||
ggml_tensor * final_output = ggml_reshape_2d(ctx0, attn_out_norm, head_v_dim * num_v_heads, n_seq_tokens * n_seqs);
|
||||
cb(final_output, "final_output", il);
|
||||
|
||||
cur = build_lora_mm(model.layers[il].ssm_out, final_output, model.layers[il].ssm_out_s);
|
||||
cb(cur, "linear_attn_out", il);
|
||||
|
||||
cur = ggml_reshape_2d(ctx0, cur, n_embd, n_seq_tokens * n_seqs);
|
||||
|
||||
return cur;
|
||||
}
|
||||
|
||||
|
||||
@@ -10659,7 +10659,7 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() {
|
||||
// ne[2] > 65535 exceeds the CUDA gridDim.y limit (#27901)
|
||||
test_cases.emplace_back(new test_rms_norm_mul_rope({4, 1, 65536, 1}, 1e-6f, false, false, false, GGML_ROPE_TYPE_NORMAL));
|
||||
test_cases.emplace_back(new test_rms_norm_mul_rope({4, 1, 65536, 1}, 1e-6f, false, true, false, GGML_ROPE_TYPE_NEOX));
|
||||
for (int64_t d_conv : {3, 4, 9}) {
|
||||
for (int64_t d_conv : {3, 4, 7, 9}) {
|
||||
for (int64_t d_inner: {1024, 1536, 2048}) {
|
||||
test_cases.emplace_back(new test_ssm_conv(GGML_TYPE_F32, {d_conv, d_inner, 1, 1}, {d_conv, d_inner, 1, 1}));
|
||||
test_cases.emplace_back(new test_ssm_conv(GGML_TYPE_F32, {2 * d_conv, d_inner, 1, 1}, {d_conv, d_inner, 1, 1}));
|
||||
@@ -10672,7 +10672,7 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() {
|
||||
|
||||
// fused ssm_conv + (optional) bias_add + silu. The bias-only graph (no silu) is intentionally
|
||||
// not tested since there's no fusion for that pattern in ggml_cuda_can_fuse.
|
||||
for (int64_t d_conv : {3, 4, 9}) {
|
||||
for (int64_t d_conv : {3, 4, 7, 9}) {
|
||||
for (int64_t d_inner : {1024, 1536, 2048}) {
|
||||
for (bool fuse_bias : {false, true}) {
|
||||
// short token path (n_t <= 32)
|
||||
@@ -12199,6 +12199,17 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_perf() {
|
||||
}
|
||||
}
|
||||
|
||||
// POOL_1D max over MoE down-projection amax shapes:
|
||||
// amax is [1, n_expert, n_tokens] after reducing the whole intermediate dim.
|
||||
for (int64_t n_ff : { 2048, 4096, 8192 }) {
|
||||
for (int64_t n_expert : { 8, 16 }) {
|
||||
for (int64_t n_tokens : { 1, 512 }) {
|
||||
test_cases.emplace_back(new test_pool1d(
|
||||
GGML_OP_POOL_MAX, GGML_TYPE_F32, {n_ff, n_expert, n_tokens, 1}, n_ff, n_ff, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LEAKY_RELU at FFN activation width, for direct comparison with RELU
|
||||
for (int64_t n_tokens : {512, 2048}) {
|
||||
test_cases.emplace_back(new test_leaky_relu(GGML_TYPE_F32, { 17408, n_tokens, 1, 1 }, 0.1f));
|
||||
|
||||
@@ -91,11 +91,19 @@ MAKE_TEST(test_image_preprocessor_dyn_size) {
|
||||
}
|
||||
}
|
||||
|
||||
static std::string image_size_to_str(const clip_image_size & size) {
|
||||
return std::to_string(size.width) + "x" + std::to_string(size.height);
|
||||
}
|
||||
|
||||
MAKE_TEST(test_image_preprocessor_lfm2) {
|
||||
clip_hparams hparams;
|
||||
hparams.patch_size = 16;
|
||||
hparams.n_merge = 2;
|
||||
hparams.set_limit_image_tokens(64, 256);
|
||||
hparams.preproc_min_tiles = 2;
|
||||
hparams.preproc_max_tiles = 10;
|
||||
hparams.preproc_tile_size = 512;
|
||||
hparams.preproc_max_pixels_tolerance = 2.0f;
|
||||
|
||||
// { image size, expected tiling }
|
||||
const std::vector<std::pair<clip_image_size, bool>> cases = {
|
||||
@@ -116,6 +124,38 @@ MAKE_TEST(test_image_preprocessor_lfm2) {
|
||||
std::string(expected ? "tiled" : "single"),
|
||||
std::string(actual ? "tiled" : "single"));
|
||||
}
|
||||
|
||||
// { image size, expected size from HF smart_resize }
|
||||
const std::vector<std::pair<clip_image_size, clip_image_size>> resize_cases = {
|
||||
{ { 400, 600 }, { 384, 608 } }, // 400 / 32 = 12.5 rounds half to even
|
||||
{ { 480, 336 }, { 480, 320 } },
|
||||
{ { 64, 169 }, { 160, 416 } }, // 169 * beta / 32 is exactly 13 in double
|
||||
{ { 1024, 768 }, { 576, 416 } },
|
||||
};
|
||||
|
||||
for (const auto & [size, expected] : resize_cases) {
|
||||
t.assert_equal("smart_resize for " + image_size_to_str(size), image_size_to_str(expected),
|
||||
image_size_to_str(mtmd_image_preprocessor_lfm2::smart_resize(hparams, size)));
|
||||
}
|
||||
|
||||
// { image size, expected grid from HF }
|
||||
const std::vector<std::pair<clip_image_size, clip_image_size>> grid_cases = {
|
||||
{ { 720, 1600 }, { 2, 4 } }, // 0.45 is an exact tie between 2/4 and 2/5 in double, not in float
|
||||
{ { 420, 1440 }, { 1, 3 } },
|
||||
{ { 255, 2160 }, { 1, 8 } },
|
||||
{ { 1024, 768 }, { 3, 2 } },
|
||||
};
|
||||
|
||||
for (const auto & [size, expected] : grid_cases) {
|
||||
t.assert_equal("grid for " + image_size_to_str(size), image_size_to_str(expected),
|
||||
image_size_to_str(mtmd_image_preprocessor_lfm2::get_grid_layout(hparams, size.height, size.width)));
|
||||
}
|
||||
|
||||
// do_image_splitting=false
|
||||
hparams.preproc_min_tiles = 1;
|
||||
hparams.preproc_max_tiles = 1;
|
||||
t.assert_equal("tiling for 1024x977 with max_tiles=1", std::string("single"),
|
||||
std::string(mtmd_image_preprocessor_lfm2::should_tile(hparams, { 1024, 977 }) ? "tiled" : "single"));
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -56,6 +56,8 @@
|
||||
#define KEY_PREPROC_MIN_TILES "clip.vision.preproc_min_tiles"
|
||||
#define KEY_PREPROC_MAX_TILES "clip.vision.preproc_max_tiles"
|
||||
#define KEY_PREPROC_IMAGE_SIZE "clip.vision.preproc_image_size"
|
||||
#define KEY_PREPROC_TILE_SIZE "clip.vision.preproc_tile_size"
|
||||
#define KEY_PREPROC_MAX_PIXELS_TOLERANCE "clip.vision.preproc_max_pixels_tolerance"
|
||||
#define KEY_PATCH_SIZE "clip.vision.patch_size"
|
||||
#define KEY_IMAGE_MEAN "clip.vision.image_mean"
|
||||
#define KEY_IMAGE_STD "clip.vision.image_std"
|
||||
|
||||
@@ -66,6 +66,7 @@ struct clip_hparams {
|
||||
resize_algo image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
pad_style image_resize_pad = PAD_CEIL; // padding style when resizing
|
||||
bool image_round_half_even = false; // smart_resize rounds .5 to even, like Python round() in the HF processor
|
||||
bool image_floor_before_align = false;
|
||||
std::array<uint8_t, 3> image_pad_color = {0, 0, 0};
|
||||
|
||||
// (preprocessor) for llava-uhd style models
|
||||
@@ -73,7 +74,8 @@ struct clip_hparams {
|
||||
int32_t preproc_min_tiles = 0;
|
||||
int32_t preproc_max_tiles = 0;
|
||||
int32_t max_slice_nums = 9; // llava-uhd slice cap; per-model, carried in the GGUF
|
||||
int32_t preproc_tile_size = 0; // local tile size (deepseek-ocr)
|
||||
int32_t preproc_tile_size = 0; // local tile size (deepseek-ocr, lfm2)
|
||||
float preproc_max_pixels_tolerance = 1.0f;
|
||||
resize_algo image_resize_algo_rf = RESIZE_ALGO_BICUBIC;
|
||||
resize_algo image_resize_algo_ov = RESIZE_ALGO_BICUBIC;
|
||||
pad_style image_pad_rf = PAD_CEIL; // padding style for the refined image (e.g. llava-1.6)
|
||||
|
||||
+34
-7
@@ -1593,12 +1593,20 @@ struct clip_model_loader {
|
||||
}
|
||||
hparams.image_resize_algo_rf = hparams.image_resize_algo;
|
||||
hparams.image_resize_algo_ov = hparams.image_resize_algo;
|
||||
// the tiles stretch the image to the grid (d1-omni vision.py)
|
||||
if (model.proj_type == PROJECTOR_TYPE_D1OMNI_V) {
|
||||
hparams.image_pad_rf = PAD_NONE;
|
||||
}
|
||||
// the tiles stretch the image to the grid (Lfm2VlImageProcessor, d1-omni vision.py)
|
||||
hparams.image_pad_rf = PAD_NONE;
|
||||
get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false);
|
||||
// defaults for GGUFs converted before tiling params were written
|
||||
// ref: https://huggingface.co/LiquidAI/LFM2.5-VL-1.6B/blob/main/processor_config.json
|
||||
hparams.preproc_min_tiles = 2;
|
||||
hparams.preproc_max_tiles = 10;
|
||||
hparams.preproc_tile_size = 512;
|
||||
hparams.preproc_max_pixels_tolerance = 2.0f;
|
||||
get_u32(KEY_PREPROC_MIN_TILES, hparams.preproc_min_tiles, false);
|
||||
get_u32(KEY_PREPROC_MAX_TILES, hparams.preproc_max_tiles, false);
|
||||
get_u32(KEY_PREPROC_TILE_SIZE, hparams.preproc_tile_size, false);
|
||||
get_f32(KEY_PREPROC_MAX_PIXELS_TOLERANCE, hparams.preproc_max_pixels_tolerance, false);
|
||||
GGML_ASSERT(hparams.preproc_min_tiles >= 1 && hparams.preproc_min_tiles <= hparams.preproc_max_tiles);
|
||||
hparams.set_limit_image_tokens(64, 256);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_PHI4:
|
||||
@@ -1617,7 +1625,23 @@ struct clip_model_loader {
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
hparams.rope_theta = 10000.0f;
|
||||
get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge, false);
|
||||
hparams.set_limit_image_tokens(8, 1024);
|
||||
get_u32(KEY_PREPROC_IMAGE_SIZE, hparams.image_longest_edge, false);
|
||||
if (hparams.image_longest_edge > 0) {
|
||||
const int64_t patch_size = int64_t(hparams.patch_size) * hparams.n_merge;
|
||||
if (patch_size <= 0) {
|
||||
throw std::runtime_error("Pixtral merged patch size must be positive");
|
||||
}
|
||||
const int64_t n_tokens_per_side =
|
||||
(hparams.image_longest_edge + patch_size - 1) / patch_size;
|
||||
if (n_tokens_per_side * patch_size > std::sqrt(std::numeric_limits<int>::max())) {
|
||||
throw std::runtime_error("Pixtral preprocessing image size is too large");
|
||||
}
|
||||
hparams.image_resize_pad = PAD_NONE;
|
||||
hparams.image_floor_before_align = true;
|
||||
hparams.set_limit_image_tokens(0, n_tokens_per_side * n_tokens_per_side);
|
||||
} else {
|
||||
hparams.set_limit_image_tokens(8, 1024);
|
||||
}
|
||||
hparams.set_warmup_n_tokens(256); // avoid OOM on warmup
|
||||
} break;
|
||||
case PROJECTOR_TYPE_LIGHTONOCR:
|
||||
@@ -1725,6 +1749,8 @@ struct clip_model_loader {
|
||||
case PROJECTOR_TYPE_GEMMA4UV:
|
||||
{
|
||||
hparams.rope_theta = 100.0f;
|
||||
hparams.ffn_op = FFN_GELU;
|
||||
log_ffn_op = "gelu";
|
||||
hparams.n_merge = 3; // pooling_kernel_size
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false);
|
||||
@@ -4252,8 +4278,9 @@ struct clip_init_result clip_init(const char * fname, struct clip_context_params
|
||||
struct clip_cap clip_get_cap(const char * fname) {
|
||||
clip_cap res;
|
||||
clip_model_loader loader(fname, /* skip_tensors= */ true);
|
||||
res.has_vision = loader.has_vision;
|
||||
res.has_audio = loader.has_audio;
|
||||
res.has_vision = loader.has_vision;
|
||||
res.has_audio = loader.has_audio;
|
||||
res.has_gen_audio = loader.has_gen_audio;
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
@@ -148,5 +148,6 @@ std::map<ggml_backend_dev_t, size_t> clip_get_mem_usage(const struct clip_ctx *
|
||||
struct clip_cap {
|
||||
bool has_vision;
|
||||
bool has_audio;
|
||||
bool has_gen_audio;
|
||||
};
|
||||
struct clip_cap clip_get_cap(const char * fname);
|
||||
|
||||
+138
-39
@@ -50,29 +50,38 @@ static llama_token find_special_token(const llama_vocab * vocab, const std::stri
|
||||
return LLAMA_TOKEN_NULL;
|
||||
}
|
||||
|
||||
static void put_bytes(std::vector<char> & buf, const void * p, size_t n) {
|
||||
const char * c = (const char *) p;
|
||||
buf.insert(buf.end(), c, c + n);
|
||||
}
|
||||
|
||||
// data_sz == UINT32_MAX writes the "unknown length" sentinel (streaming), same as ffmpeg does on a pipe
|
||||
static void write_wav16_header(std::vector<char> & buf, uint32_t data_sz, int32_t rate) {
|
||||
const uint32_t riff_sz = data_sz == UINT32_MAX ? UINT32_MAX : 36 + data_sz;
|
||||
const uint32_t fmt_sz = 16, byte_rate = (uint32_t) rate * 2;
|
||||
const uint16_t fmt = 1, ch = 1, align = 2, bits = 16;
|
||||
const uint32_t rate32 = (uint32_t) rate;
|
||||
put_bytes(buf, "RIFF", 4); put_bytes(buf, &riff_sz, 4); put_bytes(buf, "WAVE", 4);
|
||||
put_bytes(buf, "fmt ", 4); put_bytes(buf, &fmt_sz, 4);
|
||||
put_bytes(buf, &fmt, 2); put_bytes(buf, &ch, 2); put_bytes(buf, &rate32, 4);
|
||||
put_bytes(buf, &byte_rate, 4); put_bytes(buf, &align, 2); put_bytes(buf, &bits, 2);
|
||||
put_bytes(buf, "data", 4); put_bytes(buf, &data_sz, 4);
|
||||
}
|
||||
|
||||
static void append_wav16_pcm(std::vector<char> & buf, const float * pcm, size_t n) {
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
int16_t s = (int16_t) (std::max(-1.0f, std::min(1.0f, pcm[i])) * 32767.0f);
|
||||
put_bytes(buf, &s, 2);
|
||||
}
|
||||
}
|
||||
|
||||
static bool write_wav16(std::vector<char> & buf, const std::vector<float> & pcm, int32_t rate) {
|
||||
// RIFF chunk sizes are 32-bit; refuse to emit a file with a truncated header
|
||||
if (pcm.size() > ((size_t) UINT32_MAX - 36) / 2) {
|
||||
return false;
|
||||
}
|
||||
const uint32_t data_sz = (uint32_t) (pcm.size() * 2);
|
||||
const uint32_t riff_sz = 36 + data_sz;
|
||||
const uint32_t fmt_sz = 16, byte_rate = (uint32_t) rate * 2;
|
||||
const uint16_t fmt = 1, ch = 1, align = 2, bits = 16;
|
||||
const uint32_t rate32 = (uint32_t) rate;
|
||||
auto put = [&](const void * p, size_t n) {
|
||||
const char * c = (const char *) p;
|
||||
buf.insert(buf.end(), c, c + n);
|
||||
};
|
||||
put("RIFF", 4); put(&riff_sz, 4); put("WAVE", 4);
|
||||
put("fmt ", 4); put(&fmt_sz, 4);
|
||||
put(&fmt, 2); put(&ch, 2); put(&rate32, 4);
|
||||
put(&byte_rate, 4); put(&align, 2); put(&bits, 2);
|
||||
put("data", 4); put(&data_sz, 4);
|
||||
for (float v : pcm) {
|
||||
int16_t s = (int16_t) (std::max(-1.0f, std::min(1.0f, v)) * 32767.0f);
|
||||
put(&s, 2);
|
||||
}
|
||||
write_wav16_header(buf, (uint32_t) (pcm.size() * 2), rate);
|
||||
append_wav16_pcm(buf, pcm.data(), pcm.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -92,6 +101,8 @@ public:
|
||||
// set out_stop on end-of-speech, h_state_out must be null if no frame is generated
|
||||
virtual int32_t step_gen(llama_token sampled, const float * h_state_in, const float ** h_state_out, bool * out_stop) = 0;
|
||||
virtual int32_t get_output(int32_t * out_sample_rate, const char ** out_data, size_t * out_data_len, int64_t * out_n_samples) = 0;
|
||||
// forces any buffered codes through code2wav now, regardless of window_frames
|
||||
virtual int32_t flush() = 0;
|
||||
|
||||
protected:
|
||||
llama_context * lctx;
|
||||
@@ -121,6 +132,9 @@ public:
|
||||
prompt_batch.reset();
|
||||
n_prompt = 0;
|
||||
prompt_pos = 0;
|
||||
stream = false;
|
||||
pcm_sent = 0;
|
||||
wav_header_sent = false;
|
||||
}
|
||||
|
||||
int32_t set_input(const mtmd_helper_gen_audio_inp * inp) override {
|
||||
@@ -208,6 +222,7 @@ public:
|
||||
top_p = inp->top_p > 0 ? inp->top_p : def.top_p;
|
||||
seed = inp->seed;
|
||||
out_type = inp->out_type;
|
||||
stream = inp->stream;
|
||||
|
||||
// the prompt above holds the whole text stream up to tts_eos, so every generated
|
||||
// frame adds tts_pad on top of the codes embedding
|
||||
@@ -301,31 +316,60 @@ public:
|
||||
}
|
||||
|
||||
int32_t get_output(int32_t * out_sample_rate, const char ** out_data, size_t * out_data_len, int64_t * out_n_samples) override {
|
||||
if (!flush_gen_wav()) {
|
||||
return 1;
|
||||
*out_sample_rate = info.sample_rate;
|
||||
|
||||
if (!stream) {
|
||||
// one-shot call: force out whatever's left, regardless of window_frames
|
||||
if (!flush_gen_wav()) {
|
||||
return 1;
|
||||
}
|
||||
if (out_n_samples) {
|
||||
*out_n_samples = (int64_t) audio_pcm.size();
|
||||
}
|
||||
if (out_type == MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM) {
|
||||
*out_data = (const char *) audio_pcm.data();
|
||||
*out_data_len = audio_pcm.size() * sizeof(float);
|
||||
return 0;
|
||||
}
|
||||
out_buf.clear();
|
||||
if (!write_wav16(out_buf, audio_pcm, info.sample_rate)) {
|
||||
LOG_ERR("mtmd_helper_gen_audio: output too large for WAV\n");
|
||||
return 1;
|
||||
}
|
||||
*out_data = out_buf.data();
|
||||
*out_data_len = out_buf.size();
|
||||
return 0;
|
||||
}
|
||||
|
||||
*out_sample_rate = info.sample_rate;
|
||||
// streaming: only return audio produced since the previous call
|
||||
const size_t n_new = audio_pcm.size() - pcm_sent;
|
||||
if (out_n_samples) {
|
||||
*out_n_samples = (int64_t) audio_pcm.size();
|
||||
*out_n_samples = (int64_t) n_new;
|
||||
}
|
||||
|
||||
if (out_type == MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM) {
|
||||
*out_data = (const char *) audio_pcm.data();
|
||||
*out_data_len = audio_pcm.size() * sizeof(float);
|
||||
*out_data = (const char *) (audio_pcm.data() + pcm_sent);
|
||||
*out_data_len = n_new * sizeof(float);
|
||||
pcm_sent = audio_pcm.size();
|
||||
return 0;
|
||||
}
|
||||
|
||||
out_buf.clear();
|
||||
if (!write_wav16(out_buf, audio_pcm, info.sample_rate)) {
|
||||
LOG_ERR("mtmd_helper_gen_audio: output too large for WAV\n");
|
||||
return 1;
|
||||
if (!wav_header_sent) {
|
||||
write_wav16_header(out_buf, UINT32_MAX, info.sample_rate);
|
||||
wav_header_sent = true;
|
||||
}
|
||||
append_wav16_pcm(out_buf, audio_pcm.data() + pcm_sent, n_new);
|
||||
pcm_sent = audio_pcm.size();
|
||||
*out_data = out_buf.data();
|
||||
*out_data_len = out_buf.size();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t flush() override {
|
||||
return flush_gen_wav() ? 0 : 1;
|
||||
}
|
||||
|
||||
private:
|
||||
bool ensure_cache() {
|
||||
if (specials_ok) {
|
||||
@@ -369,7 +413,7 @@ private:
|
||||
LOG_ERR("mtmd_helper_gen_audio: mmproj has no speaker/audio encoder\n");
|
||||
return false;
|
||||
}
|
||||
const std::string marker = mtmd_default_marker();
|
||||
const std::string marker = mtmd_get_marker(mctx);
|
||||
mtmd_input_text text{ marker.c_str(), marker.size(), false, true };
|
||||
mtmd_input_chunks * chunks = mtmd_input_chunks_init();
|
||||
const mtmd_bitmap * bptr = bitmap;
|
||||
@@ -455,6 +499,9 @@ private:
|
||||
std::vector<float> h_state_buf;
|
||||
mtmd_helper_gen_audio_outtype out_type = MTMD_HELPER_GEN_AUDIO_OUTTYPE_WAV;
|
||||
std::vector<char> out_buf;
|
||||
bool stream = false;
|
||||
size_t pcm_sent = 0; // samples already returned by get_output()
|
||||
bool wav_header_sent = false;
|
||||
};
|
||||
|
||||
// settings that only live in the reference's per-pack yaml, not in the checkpoint
|
||||
@@ -506,6 +553,9 @@ public:
|
||||
chunk_idx = 0;
|
||||
n_voice_pos = 0;
|
||||
chunk_budget = 0;
|
||||
stream = false;
|
||||
pcm_sent = 0;
|
||||
wav_header_sent = false;
|
||||
}
|
||||
|
||||
int32_t set_input(const mtmd_helper_gen_audio_inp * inp) override {
|
||||
@@ -575,6 +625,7 @@ public:
|
||||
|
||||
seed = inp->seed;
|
||||
out_type = inp->out_type;
|
||||
stream = inp->stream;
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -662,31 +713,60 @@ public:
|
||||
}
|
||||
|
||||
int32_t get_output(int32_t * out_sample_rate, const char ** out_data, size_t * out_data_len, int64_t * out_n_samples) override {
|
||||
if (!flush_gen_wav()) {
|
||||
return 1;
|
||||
*out_sample_rate = info.sample_rate;
|
||||
|
||||
if (!stream) {
|
||||
// one-shot call: force out whatever's left, regardless of window_frames
|
||||
if (!flush_gen_wav()) {
|
||||
return 1;
|
||||
}
|
||||
if (out_n_samples) {
|
||||
*out_n_samples = (int64_t) audio_pcm.size();
|
||||
}
|
||||
if (out_type == MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM) {
|
||||
*out_data = (const char *) audio_pcm.data();
|
||||
*out_data_len = audio_pcm.size() * sizeof(float);
|
||||
return 0;
|
||||
}
|
||||
out_buf.clear();
|
||||
if (!write_wav16(out_buf, audio_pcm, info.sample_rate)) {
|
||||
LOG_ERR("mtmd_helper_gen_audio: output too large for WAV\n");
|
||||
return 1;
|
||||
}
|
||||
*out_data = out_buf.data();
|
||||
*out_data_len = out_buf.size();
|
||||
return 0;
|
||||
}
|
||||
|
||||
*out_sample_rate = info.sample_rate;
|
||||
// streaming: only return audio produced since the previous call
|
||||
const size_t n_new = audio_pcm.size() - pcm_sent;
|
||||
if (out_n_samples) {
|
||||
*out_n_samples = (int64_t) audio_pcm.size();
|
||||
*out_n_samples = (int64_t) n_new;
|
||||
}
|
||||
|
||||
if (out_type == MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM) {
|
||||
*out_data = (const char *) audio_pcm.data();
|
||||
*out_data_len = audio_pcm.size() * sizeof(float);
|
||||
*out_data = (const char *) (audio_pcm.data() + pcm_sent);
|
||||
*out_data_len = n_new * sizeof(float);
|
||||
pcm_sent = audio_pcm.size();
|
||||
return 0;
|
||||
}
|
||||
|
||||
out_buf.clear();
|
||||
if (!write_wav16(out_buf, audio_pcm, info.sample_rate)) {
|
||||
LOG_ERR("mtmd_helper_gen_audio: output too large for WAV\n");
|
||||
return 1;
|
||||
if (!wav_header_sent) {
|
||||
write_wav16_header(out_buf, UINT32_MAX, info.sample_rate);
|
||||
wav_header_sent = true;
|
||||
}
|
||||
append_wav16_pcm(out_buf, audio_pcm.data() + pcm_sent, n_new);
|
||||
pcm_sent = audio_pcm.size();
|
||||
*out_data = out_buf.data();
|
||||
*out_data_len = out_buf.size();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t flush() override {
|
||||
return flush_gen_wav() ? 0 : 1;
|
||||
}
|
||||
|
||||
private:
|
||||
bool ensure_cache() {
|
||||
if (specials_ok) {
|
||||
@@ -907,7 +987,7 @@ private:
|
||||
LOG_ERR("mtmd_helper_gen_audio: mmproj has no voice encoder\n");
|
||||
return false;
|
||||
}
|
||||
const std::string marker = mtmd_default_marker();
|
||||
const std::string marker = mtmd_get_marker(mctx);
|
||||
mtmd_input_text text{ marker.c_str(), marker.size(), false, true };
|
||||
mtmd_input_chunks * chunks = mtmd_input_chunks_init();
|
||||
const mtmd_bitmap * bptr = bitmap;
|
||||
@@ -989,6 +1069,9 @@ private:
|
||||
std::vector<float> h_state_buf;
|
||||
mtmd_helper_gen_audio_outtype out_type = MTMD_HELPER_GEN_AUDIO_OUTTYPE_WAV;
|
||||
std::vector<char> out_buf;
|
||||
bool stream = false;
|
||||
size_t pcm_sent = 0; // samples already returned by get_output()
|
||||
bool wav_header_sent = false;
|
||||
};
|
||||
|
||||
static std::unique_ptr<mtmd_gen_audio_pipeline> make_pipeline(llama_context * lctx, mtmd_context * mctx) {
|
||||
@@ -1017,11 +1100,20 @@ void mtmd_helper_gen_audio_free(mtmd_helper_gen_audio * ctx) {
|
||||
}
|
||||
|
||||
void mtmd_helper_gen_audio_reset(mtmd_helper_gen_audio * ctx) {
|
||||
if (ctx->pipeline) {
|
||||
if (ctx && ctx->pipeline) {
|
||||
ctx->pipeline->reset();
|
||||
}
|
||||
}
|
||||
|
||||
struct mtmd_helper_gen_audio_inp mtmd_helper_gen_audio_inp_default(void) {
|
||||
mtmd_helper_gen_audio_inp inp{};
|
||||
inp.top_k = 50;
|
||||
inp.top_p = 1.0f;
|
||||
inp.seed = UINT32_MAX; // random
|
||||
inp.out_type = MTMD_HELPER_GEN_AUDIO_OUTTYPE_WAV;
|
||||
return inp;
|
||||
}
|
||||
|
||||
int32_t mtmd_helper_gen_audio_set_input(mtmd_helper_gen_audio * ctx, const mtmd_helper_gen_audio_inp * inp) {
|
||||
if (!ctx->pipeline) {
|
||||
LOG_ERR("mtmd_helper_gen_audio: unsupported or missing gen-audio pipeline\n");
|
||||
@@ -1058,3 +1150,10 @@ int32_t mtmd_helper_gen_audio_get_output(mtmd_helper_gen_audio * ctx, int32_t *
|
||||
}
|
||||
return ctx->pipeline->get_output(out_sample_rate, out_data, out_data_len, out_n_samples);
|
||||
}
|
||||
|
||||
int32_t mtmd_helper_gen_audio_flush(mtmd_helper_gen_audio * ctx) {
|
||||
if (!ctx->pipeline) {
|
||||
return 1;
|
||||
}
|
||||
return ctx->pipeline->flush();
|
||||
}
|
||||
|
||||
@@ -204,6 +204,7 @@ enum mtmd_helper_gen_audio_outtype {
|
||||
MTMD_HELPER_GEN_AUDIO_OUTTYPE_WAV, // WAV PCM 16-bit LE, mono
|
||||
};
|
||||
struct mtmd_helper_gen_audio_inp {
|
||||
bool stream; // if true, output() must be called after each step_gen()
|
||||
llama_seq_id seq_id;
|
||||
|
||||
const char * prompt;
|
||||
@@ -219,6 +220,8 @@ struct mtmd_helper_gen_audio_inp {
|
||||
enum mtmd_helper_gen_audio_outtype out_type;
|
||||
};
|
||||
|
||||
MTMD_API struct mtmd_helper_gen_audio_inp mtmd_helper_gen_audio_inp_default(void);
|
||||
|
||||
MTMD_API mtmd_helper_gen_audio * mtmd_helper_gen_audio_init(
|
||||
struct llama_context * lctx,
|
||||
struct mtmd_context * mctx);
|
||||
@@ -250,6 +253,8 @@ MTMD_API int32_t mtmd_helper_gen_audio_step_gen(
|
||||
|
||||
// out_data valid until next get_output() or reset() call
|
||||
// out_n_samples (optional, can be NULL) receives the number of generated PCM samples
|
||||
// if inp->stream is true: returns only audio produced since the previous call, and
|
||||
// *out_data_len == 0 whenever a full window_frames batch hasn't accumulated yet
|
||||
MTMD_API int32_t mtmd_helper_gen_audio_get_output(
|
||||
mtmd_helper_gen_audio * ctx,
|
||||
int32_t * out_sample_rate,
|
||||
@@ -257,6 +262,10 @@ MTMD_API int32_t mtmd_helper_gen_audio_get_output(
|
||||
size_t * out_data_len,
|
||||
int64_t * out_n_samples);
|
||||
|
||||
// forces any buffered codes through code2wav now, regardless of window_frames;
|
||||
// call once when generation has ended, before the last get_output() in stream mode
|
||||
MTMD_API int32_t mtmd_helper_gen_audio_flush(mtmd_helper_gen_audio * ctx);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
@@ -283,8 +292,41 @@ struct mtmd_helper_gen_audio_deleter {
|
||||
};
|
||||
using gen_audio_ptr = std::unique_ptr<mtmd_helper_gen_audio, mtmd_helper_gen_audio_deleter>;
|
||||
struct gen_audio {
|
||||
|
||||
// sub-struct, RAII wrapper for mtmd_helper_gen_audio_inp
|
||||
struct inp {
|
||||
mtmd_helper_gen_audio_inp data = mtmd_helper_gen_audio_inp_default();
|
||||
std::string prompt_str;
|
||||
std::string lang_str;
|
||||
mtmd::bitmap_ptr speaker_ref_ptr;
|
||||
|
||||
inp() = default;
|
||||
inp(inp &&) = default;
|
||||
inp & operator=(inp &&) = default;
|
||||
inp(const inp &) = delete;
|
||||
inp & operator=(const inp &) = delete;
|
||||
|
||||
void set_prompt (std::string p) { prompt_str = std::move(p); }
|
||||
void set_lang (std::string l) { lang_str = std::move(l); }
|
||||
void set_speaker_ref(mtmd::bitmap_ptr bmp) { speaker_ref_ptr = std::move(bmp); }
|
||||
|
||||
// pointers are only valid as long as *this is alive
|
||||
const mtmd_helper_gen_audio_inp * get() {
|
||||
data.prompt = prompt_str.c_str();
|
||||
data.prompt_len = prompt_str.size();
|
||||
data.lang = lang_str.empty() ? nullptr : lang_str.c_str();
|
||||
data.speaker_ref = speaker_ref_ptr.get();
|
||||
return &data;
|
||||
}
|
||||
};
|
||||
|
||||
gen_audio_ptr ctx;
|
||||
gen_audio(struct llama_context * lctx, struct mtmd_context * mctx) : ctx(mtmd_helper_gen_audio_init(lctx, mctx)) {}
|
||||
void init(struct llama_context * lctx, struct mtmd_context * mctx) {
|
||||
ctx.reset(mtmd_helper_gen_audio_init(lctx, mctx));
|
||||
}
|
||||
bool valid() const {
|
||||
return ctx.get() != nullptr;
|
||||
}
|
||||
void reset() {
|
||||
mtmd_helper_gen_audio_reset(ctx.get());
|
||||
}
|
||||
@@ -300,6 +342,9 @@ struct gen_audio {
|
||||
int32_t get_output(int32_t * out_sample_rate, const char ** out_data, size_t * out_data_len, int64_t * out_n_samples = nullptr) {
|
||||
return mtmd_helper_gen_audio_get_output(ctx.get(), out_sample_rate, out_data, out_data_len, out_n_samples);
|
||||
}
|
||||
int32_t flush() {
|
||||
return mtmd_helper_gen_audio_flush(ctx.get());
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace mtmd_helper
|
||||
|
||||
+62
-22
@@ -873,16 +873,28 @@ mtmd_image_preproc_out mtmd_image_preprocessor_glm5v::preprocess(const clip_imag
|
||||
|
||||
clip_image_size mtmd_image_preprocessor_longest_edge::get_target_size(const clip_hparams & hparams, const clip_image_size & original_size) {
|
||||
GGML_ASSERT(hparams.image_longest_edge > 0);
|
||||
clip_image_size size = original_size;
|
||||
if (hparams.image_floor_before_align) {
|
||||
const double ratio = double(std::max(size.width, size.height)) / hparams.image_longest_edge;
|
||||
if (ratio > 1.0) {
|
||||
size.width = static_cast<int>(std::floor(size.width / ratio));
|
||||
size.height = static_cast<int>(std::floor(size.height / ratio));
|
||||
if (size.width <= 0 || size.height <= 0) {
|
||||
throw std::runtime_error("image dimensions must be positive after resizing");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the original pixtral model doesn't have n_merge
|
||||
const int cur_merge = hparams.n_merge == 0 ? 1 : hparams.n_merge;
|
||||
return img_tool::calc_size_preserved_ratio(
|
||||
original_size,
|
||||
size,
|
||||
{
|
||||
/* align_size */ hparams.patch_size * cur_merge,
|
||||
/* min_pixels */ std::max(0, hparams.image_min_pixels),
|
||||
/* max_pixels */ std::max(0, hparams.image_max_pixels),
|
||||
/* longest_edge */ std::min(hparams.image_longest_edge,
|
||||
std::max(original_size.width, original_size.height)),
|
||||
std::max(size.width, size.height)),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -947,18 +959,41 @@ bool mtmd_image_preprocessor_lfm2::should_tile(
|
||||
const int h_bar = std::max(hparams.patch_size, round_by_factor(original_size.height));
|
||||
const int w_bar = std::max(hparams.patch_size, round_by_factor(original_size.width));
|
||||
|
||||
return static_cast<double>(h_bar) * static_cast<double>(w_bar) >
|
||||
static_cast<double>(hparams.image_max_pixels) * max_pixels_tolerance;
|
||||
return hparams.preproc_max_tiles > 1 &&
|
||||
static_cast<double>(h_bar) * static_cast<double>(w_bar) >
|
||||
static_cast<double>(hparams.image_max_pixels) * hparams.preproc_max_pixels_tolerance;
|
||||
}
|
||||
|
||||
// same as Lfm2VlImageProcessor.smart_resize: Python's round half to even, double precision
|
||||
clip_image_size mtmd_image_preprocessor_lfm2::smart_resize(
|
||||
const clip_hparams & hparams,
|
||||
const clip_image_size & original_size) {
|
||||
const int align_size = hparams.patch_size * hparams.n_merge;
|
||||
const double height = original_size.height;
|
||||
const double width = original_size.width;
|
||||
|
||||
int h_bar = std::max(align_size, static_cast<int>(std::nearbyint(height / align_size)) * align_size);
|
||||
int w_bar = std::max(align_size, static_cast<int>(std::nearbyint(width / align_size)) * align_size);
|
||||
|
||||
if (static_cast<double>(h_bar) * w_bar > hparams.image_max_pixels) {
|
||||
const double beta = std::sqrt(height * width / hparams.image_max_pixels);
|
||||
h_bar = std::max(align_size, static_cast<int>(std::floor(height / beta / align_size)) * align_size);
|
||||
w_bar = std::max(align_size, static_cast<int>(std::floor(width / beta / align_size)) * align_size);
|
||||
} else if (static_cast<double>(h_bar) * w_bar < hparams.image_min_pixels) {
|
||||
const double beta = std::sqrt(hparams.image_min_pixels / (height * width));
|
||||
h_bar = static_cast<int>(std::ceil(height * beta / align_size)) * align_size;
|
||||
w_bar = static_cast<int>(std::ceil(width * beta / align_size)) * align_size;
|
||||
}
|
||||
|
||||
return {w_bar, h_bar};
|
||||
}
|
||||
|
||||
mtmd_image_preprocessor_llava_uhd::slice_instructions mtmd_image_preprocessor_lfm2::get_slice_instructions(const clip_image_size & original_size) const {
|
||||
mtmd_image_preprocessor_llava_uhd::slice_instructions inst;
|
||||
const int align_size = hparams.patch_size * hparams.n_merge;
|
||||
inst.overview_size = img_tool::calc_size_preserved_ratio(
|
||||
original_size,
|
||||
{ align_size, hparams.image_min_pixels, hparams.image_max_pixels, 0 });
|
||||
inst.overview_size = smart_resize(hparams, original_size);
|
||||
|
||||
const bool needs_tiling = should_tile(hparams, original_size);
|
||||
const int tile_size = hparams.preproc_tile_size;
|
||||
|
||||
if (!needs_tiling) {
|
||||
inst.refined_size = clip_image_size{0, 0};
|
||||
@@ -966,7 +1001,7 @@ mtmd_image_preprocessor_llava_uhd::slice_instructions mtmd_image_preprocessor_lf
|
||||
return inst;
|
||||
}
|
||||
|
||||
const clip_image_size grid = get_grid_layout(original_size.height, original_size.width);
|
||||
const clip_image_size grid = get_grid_layout(hparams, original_size.height, original_size.width);
|
||||
|
||||
inst.grid_size = grid;
|
||||
inst.refined_size = clip_image_size{tile_size * grid.width, tile_size * grid.height};
|
||||
@@ -994,23 +1029,26 @@ mtmd_image_preprocessor_llava_uhd::slice_instructions mtmd_image_preprocessor_lf
|
||||
return inst;
|
||||
}
|
||||
|
||||
// double precision as in Python, float breaks exact ties such as 720/1600 between 2/4 and 2/5
|
||||
clip_image_size mtmd_image_preprocessor_lfm2::find_closest_aspect_ratio(
|
||||
float aspect_ratio,
|
||||
const clip_hparams & hparams,
|
||||
double aspect_ratio,
|
||||
const std::vector<clip_image_size> & target_ratios,
|
||||
int width, int height) const {
|
||||
float best_ratio_diff = std::numeric_limits<float>::max();
|
||||
int width, int height) {
|
||||
double best_ratio_diff = std::numeric_limits<double>::max();
|
||||
clip_image_size best_ratio = {1, 1};
|
||||
const float area = static_cast<float>(width * height);
|
||||
const double area = static_cast<double>(width) * height;
|
||||
|
||||
for (const auto & ratio : target_ratios) {
|
||||
const float target_aspect_ratio = static_cast<float>(ratio.width) / ratio.height;
|
||||
const float ratio_diff = std::abs(aspect_ratio - target_aspect_ratio);
|
||||
const double target_aspect_ratio = static_cast<double>(ratio.width) / ratio.height;
|
||||
const double ratio_diff = std::abs(aspect_ratio - target_aspect_ratio);
|
||||
if (ratio_diff < best_ratio_diff) {
|
||||
best_ratio_diff = ratio_diff;
|
||||
best_ratio = ratio;
|
||||
} else if (ratio_diff == best_ratio_diff) {
|
||||
const float target_area = static_cast<float>(tile_size * tile_size * ratio.width * ratio.height);
|
||||
if (area > 0.5f * target_area) {
|
||||
const int tile_size = hparams.preproc_tile_size;
|
||||
const double target_area = static_cast<double>(tile_size * tile_size * ratio.width * ratio.height);
|
||||
if (area > 0.5 * target_area) {
|
||||
best_ratio = ratio;
|
||||
}
|
||||
}
|
||||
@@ -1018,7 +1056,9 @@ clip_image_size mtmd_image_preprocessor_lfm2::find_closest_aspect_ratio(
|
||||
return best_ratio;
|
||||
}
|
||||
|
||||
std::vector<clip_image_size> mtmd_image_preprocessor_lfm2::get_target_ratios() const {
|
||||
std::vector<clip_image_size> mtmd_image_preprocessor_lfm2::get_target_ratios(const clip_hparams & hparams) {
|
||||
const int min_tiles = hparams.preproc_min_tiles;
|
||||
const int max_tiles = hparams.preproc_max_tiles;
|
||||
std::vector<clip_image_size> ratios;
|
||||
for (int n = min_tiles; n <= max_tiles; n++) {
|
||||
for (int w = 1; w <= n; w++) {
|
||||
@@ -1044,10 +1084,10 @@ std::vector<clip_image_size> mtmd_image_preprocessor_lfm2::get_target_ratios() c
|
||||
return ratios;
|
||||
}
|
||||
|
||||
clip_image_size mtmd_image_preprocessor_lfm2::get_grid_layout(int height, int width) const {
|
||||
const float aspect_ratio = static_cast<float>(width) / height;
|
||||
const auto ratios = get_target_ratios();
|
||||
return find_closest_aspect_ratio(aspect_ratio, ratios, width, height);
|
||||
clip_image_size mtmd_image_preprocessor_lfm2::get_grid_layout(const clip_hparams & hparams, int height, int width) {
|
||||
const double aspect_ratio = static_cast<double>(width) / height;
|
||||
const auto ratios = get_target_ratios(hparams);
|
||||
return find_closest_aspect_ratio(hparams, aspect_ratio, ratios, width, height);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
+8
-11
@@ -191,27 +191,24 @@ protected:
|
||||
};
|
||||
|
||||
// custom llava-uhd slicing logic for LFM2
|
||||
// tiling params (min/max tiles, tile size, pixels tolerance) come from hparams
|
||||
// ref: https://github.com/huggingface/transformers/blob/v5.1.0/src/transformers/models/lfm2_vl/image_processing_lfm2_vl_fast.py
|
||||
struct mtmd_image_preprocessor_lfm2 : mtmd_image_preprocessor_llava_uhd {
|
||||
// ref: https://huggingface.co/LiquidAI/LFM2.5-VL-1.6B/blob/main/processor_config.json
|
||||
static constexpr int min_tiles = 2;
|
||||
static constexpr int max_tiles = 10;
|
||||
static constexpr float max_pixels_tolerance = 2.0f;
|
||||
static constexpr int tile_size = 512;
|
||||
|
||||
using mtmd_image_preprocessor_llava_uhd::mtmd_image_preprocessor_llava_uhd;
|
||||
mtmd_image_preproc_out preprocess(const clip_image_u8 & img) const override;
|
||||
slice_instructions get_slice_instructions(const clip_image_size & original_size) const override;
|
||||
|
||||
static bool should_tile(const clip_hparams & hparams, const clip_image_size & original_size);
|
||||
static clip_image_size smart_resize(const clip_hparams & hparams, const clip_image_size & original_size);
|
||||
static clip_image_size get_grid_layout(const clip_hparams & hparams, int height, int width);
|
||||
|
||||
private:
|
||||
clip_image_size find_closest_aspect_ratio(
|
||||
float aspect_ratio,
|
||||
static clip_image_size find_closest_aspect_ratio(
|
||||
const clip_hparams & hparams,
|
||||
double aspect_ratio,
|
||||
const std::vector<clip_image_size> & target_ratios,
|
||||
int width, int height) const;
|
||||
std::vector<clip_image_size> get_target_ratios() const;
|
||||
clip_image_size get_grid_layout(int height, int width) const;
|
||||
int width, int height);
|
||||
static std::vector<clip_image_size> get_target_ratios(const clip_hparams & hparams);
|
||||
};
|
||||
|
||||
struct mtmd_image_preprocessor_idefics3 : mtmd_image_preprocessor_llava_uhd {
|
||||
|
||||
+7
-2
@@ -804,7 +804,11 @@ struct mtmd_context {
|
||||
{
|
||||
// https://github.com/huggingface/transformers/blob/1cd110c6cb6a6237614130c470e9a902dbc1a4bd/docs/source/en/model_doc/pixtral.md
|
||||
img_end = "[IMG_END]";
|
||||
image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
|
||||
if (clip_get_hparams(ctx_v)->image_longest_edge > 0) {
|
||||
image_preproc = std::make_unique<mtmd_image_preprocessor_longest_edge>(ctx_v);
|
||||
} else {
|
||||
image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
|
||||
}
|
||||
} break;
|
||||
case PROJECTOR_TYPE_PHI4:
|
||||
{
|
||||
@@ -2842,10 +2846,11 @@ struct mtmd_caps mtmd_get_cap_from_file(const char * fname) {
|
||||
mtmd_caps cap;
|
||||
cap.inp_audio = tmp.has_audio;
|
||||
cap.inp_vision = tmp.has_vision;
|
||||
cap.gen_audio = tmp.has_gen_audio;
|
||||
return cap;
|
||||
} catch (const std::exception & e) {
|
||||
LOG_ERR("%s: failed to get capabilities from file '%s': %s\n", __func__, fname, e.what());
|
||||
return mtmd_caps{ false, false };
|
||||
return mtmd_caps{ false, false, false };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -360,6 +360,7 @@ MTMD_API void mtmd_log_set(ggml_log_callback log_callback, void * user_data);
|
||||
struct mtmd_caps {
|
||||
bool inp_vision;
|
||||
bool inp_audio;
|
||||
bool gen_audio;
|
||||
};
|
||||
MTMD_API struct mtmd_caps mtmd_get_cap_from_file(const char * mmproj_fname);
|
||||
|
||||
|
||||
+79
-5
@@ -802,6 +802,50 @@ curl http://127.0.0.1:8012/v1/rerank \
|
||||
}' | jq
|
||||
```
|
||||
|
||||
### POST `/tts`: Generate speech audio from text
|
||||
|
||||
Returns raw audio bytes (`audio/wav` by default) rather than JSON. For more info, see [tts/README.md](../tts/README.md)
|
||||
|
||||
*Options:*
|
||||
|
||||
`input`: The text to speak (alias: `prompt`).
|
||||
|
||||
`lang`: Language code for the utterance (model-dependent, e.g. `en`, `zh`). Optional.
|
||||
|
||||
`speaker_ref_b64`: Base64-encoded reference audio to clone the speaker's voice. Optional. Alternatively, upload the reference audio as a `speaker_ref` file field via `multipart/form-data` (see example below) - if both are provided, the uploaded file takes precedence.
|
||||
|
||||
`top_k`, `top_p`: Sampling params for the acoustic code predictor. Optional, model-dependent defaults apply.
|
||||
|
||||
`repeat_penalty`: Repetition penalty applied to the backbone sampler over the whole generation (default `1.05`). Without this, the backbone can loop and re-generate the same utterance.
|
||||
|
||||
`seed`: RNG seed for both the backbone sampler and the codec/vocoder (`-1` = random, the default). Set to a fixed value for reproducible output.
|
||||
|
||||
`n_predict`: Max number of audio frames to generate. Defaults to `512`; generation normally stops earlier once the model emits an end-of-speech token.
|
||||
|
||||
`response_format`: `wav` (default) or `pcm` (raw `float32` little-endian mono samples, no header; the response `Content-Type` carries the sample rate, e.g. `audio/pcm;rate=24000;encoding=float;bits=32`).
|
||||
|
||||
`stream`: If `true`, the response is streamed as audio becomes available instead of waiting for the full generation to finish. WAV streaming writes an RFC-noncompliant header with an unknown (`0xFFFFFFFF`) size field, since the final length isn't known up front; most players and decoders (ffmpeg, VLC, ...) handle this by reading until EOF.
|
||||
|
||||
Note: it's highly recommended to always provide a speaker reference voice; otherwise, the model's performance may be degraded.
|
||||
|
||||
*Examples:*
|
||||
|
||||
```shell
|
||||
curl -X POST http://127.0.0.1:9931/tts \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"input": "Hello, this is a test."}' \
|
||||
-o output.wav
|
||||
```
|
||||
|
||||
With a speaker reference uploaded as a file (`multipart/form-data`), instead of base64-encoding it into the JSON body:
|
||||
|
||||
```shell
|
||||
curl -X POST http://127.0.0.1:9931/tts \
|
||||
-F "input=Hello, this is a test." \
|
||||
-F "speaker_ref=@/path/to/speaker-reference.wav;type=audio/wav" \
|
||||
-o output.wav
|
||||
```
|
||||
|
||||
### POST `/infill`: For code infilling.
|
||||
|
||||
Takes a prefix and a suffix and returns the predicted completion as stream.
|
||||
@@ -1262,11 +1306,12 @@ Each object in `data` has an `architecture` object. It has two string arrays:
|
||||
- `input_modalities` lists what the model can read. It always has `text`, plus each media type that the model supports.
|
||||
- `output_modalities` lists what the model can produce.
|
||||
|
||||
One output value is special:
|
||||
Some output values are special:
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| `decisions` | The model is a native decision model. Serve it with [`/v1/systemone`](#post-v1systemone-typesafe-compatible-system-one-api). |
|
||||
| `audio` | The model can generate speech. Serve it with [`/tts`](#post-tts-generate-speech-audio-from-text). A TTS-only model has `["audio"]` without `text`. |
|
||||
|
||||
A language model that classifies with prompts does not get `decisions`. Only native decision models do.
|
||||
|
||||
@@ -1491,6 +1536,31 @@ Acts on an in-flight completion identified by its `id` (the `id` field streamed
|
||||
|
||||
Returns a JSON object with a boolean `success` field, and an optional `message` field describing the reason when `success` is `false`.
|
||||
|
||||
### WebSocket `/v1/realtime`: Realtime transcription
|
||||
|
||||
Transcribes streamed audio, for models with a streaming audio encoder. Compatible with OAI `/v1/realtime` schema.
|
||||
|
||||
Audio must be PCM16, mono, at the model sample rate. The sample rate is returned in `session.created` as `audio.input.format.rate`. Each `input_audio_buffer.append` carries a base64-encoded chunk of PCM16 samples.
|
||||
|
||||
*Client events:*
|
||||
|
||||
- `session.update`: accepted for compatibility, the server replies with `session.updated`
|
||||
- `input_audio_buffer.append`: `audio` holds base64 PCM16 samples
|
||||
- `input_audio_buffer.commit`: flushes the audio received so far, so the words still held back by the model's delay are produced. The server replies with `input_audio_buffer.committed` and starts a new item. With `"final": true`, the input ends instead and the last item is completed once the stream drains.
|
||||
- `input_audio_buffer.clear`: accepted for compatibility, audio already sent cannot be discarded
|
||||
|
||||
Closing the websocket also ends the input.
|
||||
|
||||
*Server events:*
|
||||
|
||||
- `session.created` and `session.updated`: carry the `session` object
|
||||
- `conversation.item.input_audio_transcription.delta`: `delta` holds the new text for `item_id`
|
||||
- `conversation.item.input_audio_transcription.completed`: `transcript` holds the full text of `item_id`
|
||||
- `input_audio_buffer.committed`: `item_id` of the committed item, `previous_item_id` of the item committed before it (`null` for the first one)
|
||||
- `error`: `error.message` describes the problem
|
||||
|
||||
Note that words held back by the model's delay at the time of a commit are attributed to the next item.
|
||||
|
||||
### POST `/v1/responses`: OpenAI-compatible Responses API
|
||||
|
||||
*Options:*
|
||||
@@ -1725,13 +1795,13 @@ Follows the [TypeSafe API](https://docs.typesafe.ai/api), streaming is not suppo
|
||||
|
||||
The questions of a request are answered independently, an answer does not depend on the other questions. The exception is clef: it reads all the questions in one prompt and decides them jointly.
|
||||
|
||||
The number of options of a `choice` question is limited by the model, for example: 52 for openjev, 255 for granite-switch, laya, clef, pplx-decider, vllm-decision2, vllm-d3, lfm2-d1 and lfm2-d1-omni. For laya, long questions and options are truncated to the token budget the model was trained with.
|
||||
The number of options of a `choice` question is limited by the model, for example: 52 for openjev, 255 for granite-switch, laya, clef, pplx-decider, vllm-decision2, vllm-d3, lfm2-d1, lfm2-d1-omni and h2o-lightning. For laya, long questions and options are truncated to the token budget the model was trained with.
|
||||
|
||||
For laya, clef, vllm-d3 and lfm2-d1-omni, the whole prompt is evaluated in one batch: it must fit in `--ubatch-size`. An lfm2-d1-omni prompt is cut to 16384 tokens. For vllm-decision2, the options of a question and the end of the prompt must fit in one batch. A server that runs clef only serves this endpoint, text generation is not available. A decision model that is also a text model, such as granite-switch, serves both and reports `["text", "decisions"]`.
|
||||
For laya, clef, vllm-d3 and lfm2-d1-omni, the whole prompt is evaluated in one batch: it must fit in `--ubatch-size`. An lfm2-d1-omni prompt is cut to 16384 tokens. For vllm-decision2, the options of a question and the end of the prompt must fit in one batch. An h2o-lightning state longer than 32000 tokens keeps its first and last 16000 tokens. A server that runs clef only serves this endpoint, text generation is not available. A decision model that is also a text model, such as granite-switch, serves both and reports `["text", "decisions"]`.
|
||||
|
||||
*Image input:*
|
||||
|
||||
Image input needs a model that supports it (for example: openjev, clef, pplx-decider, vllm-d3, lfm2-d1, lfm2-d1-omni) and its multimodal projector, see `--mmproj`.
|
||||
Image input needs a model that supports it (for example: openjev, clef, pplx-decider, vllm-d3, lfm2-d1, lfm2-d1-omni, h2o-lightning) and its multimodal projector, see `--mmproj`.
|
||||
|
||||
Images can be given in two ways, and both can be used in the same request:
|
||||
|
||||
@@ -1760,6 +1830,8 @@ All the images are placed before the state in the prompt, the ones from `files`
|
||||
|
||||
The probabilities are scaled with the temperatures stored in the model file. They are not guaranteed to be calibrated for your data.
|
||||
|
||||
For h2o-lightning, `score` questions can have 1 to 255 levels, and the `instructions` and `type` of a question can be left out (`choice` by default; `"instructions": null` is written as `null`).
|
||||
|
||||
*Examples:*
|
||||
|
||||
```shell
|
||||
@@ -1990,6 +2062,8 @@ For **GET** endpoints (`/props`, `/metrics`, etc.) The router uses the `model` q
|
||||
GET /props?model=ggml-org%2Fgemma-3-4b-it-GGUF%3AQ4_K_M
|
||||
```
|
||||
|
||||
For the **WebSocket** endpoint (`/v1/realtime`) The router also uses the `model` query parameter.
|
||||
|
||||
By default, the model will be loaded automatically if it's not loaded. To disable this, add `--no-models-autoload` when starting the server. Additionally, you can include `?autoload=true|false` in the query param to control this behavior per-request.
|
||||
|
||||
### GET `/models`: List available models
|
||||
@@ -2025,7 +2099,7 @@ Note:
|
||||
- If a model is not running, it will be added or updated according to the source
|
||||
2. When the model is loaded, the info from `/v1/models` is forwarded to router's `/v1/models`. This includes metadata about the model and the runtime instance.
|
||||
|
||||
Each object in `data` has the same `architecture` object as [`GET /v1/models`](#get-v1models-openai-compatible-model-info-api) of a direct server. The server computes both arrays offline. It does not load the model, download files, or run inference. `output_modalities` comes from the GGUF metadata. `input_modalities` comes from the projector file. A native decision model shows `decisions` before its first load, after unload, and while it sleeps:
|
||||
Each object in `data` has the same `architecture` object as [`GET /v1/models`](#get-v1models-openai-compatible-model-info-api) of a direct server. The server computes both arrays offline. It does not load the model, download files, or run inference. `output_modalities` comes from the GGUF metadata, plus `audio` when the projector file can generate speech. `input_modalities` comes from the projector file. A native decision model shows `decisions` before its first load, after unload, and while it sleeps:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -202,7 +202,8 @@ const char * get_media_marker() {
|
||||
// model output modalities
|
||||
//
|
||||
|
||||
std::vector<std::string> server_model_output_modalities(common_decision_type decision_type) {
|
||||
std::vector<std::string> server_model_output_modalities(common_decision_type decision_type, bool has_cap_chat, bool has_cap_tts) {
|
||||
std::vector<std::string> out;
|
||||
switch (decision_type) {
|
||||
case COMMON_DECISION_TYPE_OPENJEV:
|
||||
case COMMON_DECISION_TYPE_LEV:
|
||||
@@ -215,13 +216,28 @@ std::vector<std::string> server_model_output_modalities(common_decision_type dec
|
||||
case COMMON_DECISION_TYPE_VLLM_D3:
|
||||
case COMMON_DECISION_TYPE_LFM2_D1:
|
||||
case COMMON_DECISION_TYPE_LFM2_D1_OMNI:
|
||||
return {"decisions"};
|
||||
case COMMON_DECISION_TYPE_H2O_LIGHTNING:
|
||||
out = {"decisions"};
|
||||
break;
|
||||
case COMMON_DECISION_TYPE_GRANITE_SWITCH:
|
||||
return {"text", "decisions"};
|
||||
out = {"text", "decisions"};
|
||||
break;
|
||||
default:
|
||||
// fallback when there is no decision type or the metadata is bad
|
||||
return {"text"};
|
||||
// a tts-only model cannot generate text
|
||||
if (has_cap_chat || !has_cap_tts) {
|
||||
// fallback when there is no decision type or the metadata is bad
|
||||
out = {"text"};
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (has_cap_tts) {
|
||||
out.push_back("audio");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<std::string> server_model_output_modalities(const llama_model * model, bool has_cap_chat, bool has_cap_tts) {
|
||||
return server_model_output_modalities(common_get_decision_type(model), has_cap_chat, has_cap_tts);
|
||||
}
|
||||
|
||||
json server_model_architecture_json(
|
||||
|
||||
@@ -114,7 +114,10 @@ const char * get_media_marker();
|
||||
//
|
||||
|
||||
// output modalities for architecture.output_modalities in GET /models
|
||||
std::vector<std::string> server_model_output_modalities(common_decision_type decision_type);
|
||||
// has_cap_chat: false for a tts-only model, has_cap_tts: the model can generate audio
|
||||
std::vector<std::string> server_model_output_modalities(common_decision_type decision_type, bool has_cap_chat, bool has_cap_tts);
|
||||
// same, with the decision type read from a loaded model
|
||||
std::vector<std::string> server_model_output_modalities(const llama_model * model, bool has_cap_chat, bool has_cap_tts);
|
||||
|
||||
// architecture object of GET /models; shared by the direct server and the router
|
||||
json server_model_architecture_json(
|
||||
|
||||
+678
-27
@@ -1,4 +1,7 @@
|
||||
#include "server-context.h"
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include "base64.hpp"
|
||||
#include "server-chat.h"
|
||||
#include "server-common.h"
|
||||
#include "server-decision.h"
|
||||
@@ -17,6 +20,7 @@
|
||||
#include "speculative.h"
|
||||
#include "mtmd.h"
|
||||
#include "mtmd-helper.h"
|
||||
#include "base64.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
@@ -40,6 +44,14 @@
|
||||
constexpr int HTTP_POLLING_SECONDS = 1;
|
||||
|
||||
static common_speculative_output_limits server_output_limits(const common_params & params) {
|
||||
if (!params.mmproj.path.empty()) {
|
||||
const auto mcaps = mtmd_get_cap_from_file(params.mmproj.path.c_str());
|
||||
if (mcaps.gen_audio) {
|
||||
// some TTS models decode with embeddings output for the whole batch
|
||||
return { params.n_batch, params.n_batch };
|
||||
}
|
||||
}
|
||||
|
||||
if (params.embedding ||
|
||||
(params.pooling_type != LLAMA_POOLING_TYPE_UNSPECIFIED && params.pooling_type != LLAMA_POOLING_TYPE_NONE)) {
|
||||
return { params.n_batch, 1 };
|
||||
@@ -254,6 +266,27 @@ struct server_slot {
|
||||
mtmd_context * mctx = nullptr;
|
||||
mtmd::batch_ptr mbatch = nullptr;
|
||||
|
||||
struct tts_ctx {
|
||||
mtmd_helper::gen_audio ctx;
|
||||
std::vector<float> h_state_prompt; // the first h_state after step_prompt done
|
||||
const float * h_state;
|
||||
llama_token sampled;
|
||||
int32_t n_decoded;
|
||||
bool stop;
|
||||
bool is_supported() const {
|
||||
return ctx.valid();
|
||||
}
|
||||
void reset() {
|
||||
ctx.reset();
|
||||
h_state_prompt.clear();
|
||||
h_state = nullptr;
|
||||
sampled = LLAMA_TOKEN_NULL;
|
||||
n_decoded = 0;
|
||||
stop = false;
|
||||
}
|
||||
};
|
||||
tts_ctx tts;
|
||||
|
||||
// speculative decoding
|
||||
common_speculative * spec;
|
||||
|
||||
@@ -360,8 +393,13 @@ struct server_slot {
|
||||
|
||||
// for TTS models, this is the embd generated from prev step, decode this to generate next hidden state
|
||||
// corresponding to one token position (size = n_embd)
|
||||
// for realtime transcription, these are the rows from the last audio stream step (size = n_rows * n_embd)
|
||||
std::vector<float> inp_embd;
|
||||
|
||||
// realtime transcription, see SERVER_TASK_TYPE_TRANSCRIBE_RT
|
||||
mtmd::audio_stream_ptr audio_stream;
|
||||
bool audio_wait = false; // the stream needs more audio before it can produce rows
|
||||
|
||||
server_slot_stats stats;
|
||||
|
||||
// accepted tokens per draft position
|
||||
@@ -413,6 +451,12 @@ struct server_slot {
|
||||
|
||||
// clear multimodal state
|
||||
mbatch.reset();
|
||||
|
||||
tts.reset();
|
||||
|
||||
audio_stream.reset();
|
||||
audio_wait = false;
|
||||
inp_embd.clear();
|
||||
}
|
||||
|
||||
void init_sampler() const {
|
||||
@@ -542,16 +586,28 @@ struct server_slot {
|
||||
// add sampled token of this slot to the batch, optionally add the speculative draft tokens if any
|
||||
void handle_last_sampled_token(server_batch & batch) {
|
||||
bool add_ok = true;
|
||||
// the audio stream has no token before the first step, track the position with a dummy token
|
||||
const llama_token tok_track = sampled == LLAMA_TOKEN_NULL ? 0 : sampled;
|
||||
if (spec_draft.empty()) {
|
||||
// no speculative decoding
|
||||
i_batch = batch.size();
|
||||
|
||||
if (!inp_embd.empty()) {
|
||||
// text embeddings broadcast the same position across the M-RoPE sections
|
||||
const llama_pos p = prompt.tokens.pos_next();
|
||||
const llama_pos pos[GGML_MROPE_SECTIONS] = { p, p, p, 0 };
|
||||
GGML_ASSERT((int32_t) inp_embd.size() == batch.n_embd);
|
||||
add_ok &= batch.add_embd(id, inp_embd.data(), pos, true, false);
|
||||
const int32_t n_rows = (int32_t) inp_embd.size() / batch.n_embd;
|
||||
GGML_ASSERT((int32_t) inp_embd.size() == n_rows * batch.n_embd);
|
||||
i_batch = batch.size() + n_rows - 1;
|
||||
for (int32_t i = 0; i < n_rows; i++) {
|
||||
// text embeddings broadcast the same position across the M-RoPE sections
|
||||
const llama_pos p = prompt.tokens.pos_next();
|
||||
const llama_pos pos[GGML_MROPE_SECTIONS] = { p, p, p, 0 };
|
||||
add_ok &= batch.add_embd(id, inp_embd.data() + (size_t) i * batch.n_embd, pos, i == n_rows - 1, false);
|
||||
if (i < n_rows - 1) {
|
||||
prompt.tokens.push_back(tok_track);
|
||||
}
|
||||
}
|
||||
if (audio_stream) {
|
||||
inp_embd.clear(); // rows are consumed, the next step produces new ones
|
||||
}
|
||||
} else {
|
||||
add_ok &= batch.add(id, sampled, prompt.tokens.pos_next(), true, false);
|
||||
}
|
||||
@@ -579,7 +635,7 @@ struct server_slot {
|
||||
|
||||
GGML_ASSERT(add_ok && "batch must be large enough to hold the sampled and draft tokens");
|
||||
|
||||
prompt.tokens.push_back(sampled);
|
||||
prompt.tokens.push_back(tok_track);
|
||||
prompt.tokens.insert(spec_draft);
|
||||
}
|
||||
|
||||
@@ -595,7 +651,12 @@ struct server_slot {
|
||||
|
||||
// do not keep context of the child slots - the parent's context is enough
|
||||
// nor of the stateless slots - no task ever reuses it
|
||||
if (task->is_child() || task->is_stateless()) {
|
||||
if (task->is_child() || task->is_stateless() || audio_stream) {
|
||||
prompt_clear();
|
||||
}
|
||||
|
||||
// mtmd_helper always re-eval the whole prompt
|
||||
if (task->type == SERVER_TASK_TYPE_TTS) {
|
||||
prompt_clear();
|
||||
}
|
||||
|
||||
@@ -952,6 +1013,14 @@ public:
|
||||
mtmd_helper_init_opt init_opt = mtmd_helper_init_opt_default();
|
||||
const llama_vocab * vocab = nullptr;
|
||||
|
||||
bool has_cap_tts() const {
|
||||
return mctx != nullptr && mtmd_gen_audio_get_info(mctx).type != MTMD_GEN_AUDIO_TYPE_NONE;
|
||||
}
|
||||
|
||||
bool has_cap_chat() const {
|
||||
return mctx == nullptr || mtmd_helper_model_can_chat(ctx_tgt, mctx);
|
||||
}
|
||||
|
||||
server_decision_context decision;
|
||||
|
||||
server_queue queue_tasks;
|
||||
@@ -1180,7 +1249,8 @@ private:
|
||||
}
|
||||
|
||||
// optionally fit mmproj memory usage
|
||||
if (has_mmproj && params_base.fit_params) {
|
||||
// on resume params_base already holds the margin from the first load
|
||||
if (has_mmproj && params_base.fit_params && !is_resume) {
|
||||
const auto & mmproj_mem = mmproj_usage.backend_mem_usage;
|
||||
const int64_t t_elapsed = mmproj_usage_t_us;
|
||||
if (!mmproj_mem.empty()) {
|
||||
@@ -1439,6 +1509,10 @@ private:
|
||||
slot.mctx = mctx;
|
||||
slot.prompt.tokens.has_mtmd = mctx != nullptr;
|
||||
|
||||
if (has_cap_tts()) {
|
||||
slot.tts.ctx.init(ctx_tgt, mctx);
|
||||
}
|
||||
|
||||
SLT_TRC(slot, "new slot, n_ctx = %d\n", slot.n_ctx);
|
||||
|
||||
slot.callback_on_release = [this](int id_slot) {
|
||||
@@ -1920,6 +1994,20 @@ private:
|
||||
|
||||
SLT_DBG(slot, "launching slot : %s\n", safe_json_to_str(slot.to_json()).c_str());
|
||||
|
||||
if (task.type == SERVER_TASK_TYPE_TTS) {
|
||||
GGML_ASSERT(has_cap_tts()); // should already checked in route handler
|
||||
if (!slot.tts.is_supported()) {
|
||||
slot.tts.ctx.init(ctx_tgt, slot.mctx);
|
||||
}
|
||||
// mtmd_helper always re-eval the whole prompt
|
||||
slot.prompt_clear();
|
||||
task.tts_inp.data.seq_id = slot.id;
|
||||
if (slot.tts.ctx.set_input(task.tts_inp.get()) != 0) {
|
||||
send_error(task, "failed to process TTS prompt", ERROR_TYPE_SERVER);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// initialize samplers
|
||||
if (task.need_sampling()) {
|
||||
try {
|
||||
@@ -1937,6 +2025,9 @@ private:
|
||||
// TODO: getting pre sampling logits is not yet supported with backend sampling
|
||||
use_backend_sampling &= !need_pre_sample_logits;
|
||||
|
||||
// TODO: check verify if this actually works with TTS
|
||||
use_backend_sampling &= task.type != SERVER_TASK_TYPE_TTS;
|
||||
|
||||
// TODO: tmp until backend sampling is fully implemented
|
||||
if (use_backend_sampling) {
|
||||
llama_set_sampler(ctx_tgt, slot.id, common_sampler_get(slot.smpl.get()));
|
||||
@@ -1962,9 +2053,30 @@ private:
|
||||
|
||||
slot.task = std::make_unique<const server_task>(std::move(task));
|
||||
|
||||
slot.state = slot.task->is_child()
|
||||
? SLOT_STATE_WAIT_OTHER // wait for the parent to process prompt
|
||||
: SLOT_STATE_STARTED;
|
||||
if (slot.task->type == SERVER_TASK_TYPE_TTS) {
|
||||
slot.state = SLOT_STATE_PROCESSING_PROMPT;
|
||||
} else {
|
||||
slot.state = slot.task->is_child()
|
||||
? SLOT_STATE_WAIT_OTHER // wait for the parent to process prompt
|
||||
: SLOT_STATE_STARTED;
|
||||
}
|
||||
|
||||
// no prompt, the rows come from the audio stream, which audio controls reach from now on
|
||||
if (slot.task->type == SERVER_TASK_TYPE_TRANSCRIBE_RT) {
|
||||
slot.audio_stream.reset(mtmd_audio_stream_init(mctx, mtmd_audio_stream_params_default()));
|
||||
if (!slot.audio_stream) {
|
||||
send_error(slot, "failed to initialize the audio stream", ERROR_TYPE_SERVER);
|
||||
slot.release();
|
||||
return false;
|
||||
}
|
||||
slot.prompt_clear();
|
||||
slot.init_sampler();
|
||||
slot.sampled = LLAMA_TOKEN_NULL;
|
||||
slot.i_batch = -1;
|
||||
slot.stats.update_prompt_start();
|
||||
slot.state = SLOT_STATE_GENERATING;
|
||||
send_partial_response(slot, {}, false, true);
|
||||
}
|
||||
|
||||
// reset server kill-switch counter
|
||||
n_empty_consecutive = 0;
|
||||
@@ -2227,6 +2339,18 @@ private:
|
||||
queue_results.send(std::move(res));
|
||||
}
|
||||
|
||||
void send_tts_result(server_slot & slot, int32_t sample_rate, const char * data, size_t data_len, bool final) {
|
||||
auto res = std::make_unique<server_task_result_tts>();
|
||||
|
||||
res->id = slot.task->id;
|
||||
res->index = slot.task->index;
|
||||
res->sample_rate = sample_rate;
|
||||
res->audio.assign(data, data_len);
|
||||
res->final = final;
|
||||
|
||||
queue_results.send(std::move(res));
|
||||
}
|
||||
|
||||
void send_final_response(server_slot & slot) {
|
||||
auto res = std::make_unique<server_task_result_cmpl_final>();
|
||||
|
||||
@@ -2828,8 +2952,10 @@ private:
|
||||
switch (task.type) {
|
||||
case SERVER_TASK_TYPE_COMPLETION:
|
||||
case SERVER_TASK_TYPE_INFILL:
|
||||
case SERVER_TASK_TYPE_TRANSCRIBE_RT:
|
||||
case SERVER_TASK_TYPE_EMBEDDING:
|
||||
case SERVER_TASK_TYPE_RERANK:
|
||||
case SERVER_TASK_TYPE_TTS:
|
||||
case SERVER_TASK_TYPE_DECISION:
|
||||
{
|
||||
// special case: if input is provided via CLI, tokenize it first
|
||||
@@ -2915,15 +3041,15 @@ private:
|
||||
|
||||
server_slot * slot = get_slot_by_cmpl_id(task.params.control_cmpl_id);
|
||||
if (slot == nullptr) {
|
||||
SRV_WRN("control %s on unknown completion id=%s, no live slot\n",
|
||||
task.params.control_action.c_str(), task.params.control_cmpl_id.c_str());
|
||||
SRV_WRN("control %d on unknown completion id=%s, no live slot\n",
|
||||
(int) task.params.control_action, task.params.control_cmpl_id.c_str());
|
||||
res->success = false;
|
||||
res->message = "no active completion for this id";
|
||||
queue_results.send(std::move(res));
|
||||
break;
|
||||
}
|
||||
|
||||
if (task.params.control_action == "reasoning_end") {
|
||||
if (task.params.control_action == SERVER_CONTROL_ACTION_REASONING_END) {
|
||||
// the budget sampler only exists when reasoning control was armed
|
||||
if (!slot->task->params.sampling.reasoning_control) {
|
||||
res->success = false;
|
||||
@@ -2934,6 +3060,22 @@ private:
|
||||
// act on the live slot mid generation, never defer
|
||||
common_sampler_reasoning_budget_force(slot->smpl.get());
|
||||
res->success = true;
|
||||
} else if (task.params.control_action == SERVER_CONTROL_ACTION_AUDIO_APPEND
|
||||
|| task.params.control_action == SERVER_CONTROL_ACTION_AUDIO_COMMIT
|
||||
|| task.params.control_action == SERVER_CONTROL_ACTION_AUDIO_END) {
|
||||
if (!slot->audio_stream) {
|
||||
res->success = false;
|
||||
res->message = "not a realtime transcription slot";
|
||||
} else {
|
||||
auto * stream = slot->audio_stream.get();
|
||||
if (task.params.control_action == SERVER_CONTROL_ACTION_AUDIO_APPEND) {
|
||||
mtmd_audio_stream_push(stream, task.params.control_audio.data(), task.params.control_audio.size());
|
||||
} else {
|
||||
mtmd_audio_stream_flush(stream, task.params.control_action == SERVER_CONTROL_ACTION_AUDIO_END);
|
||||
}
|
||||
slot->audio_wait = false;
|
||||
res->success = true;
|
||||
}
|
||||
} else {
|
||||
res->success = false;
|
||||
res->message = "unknown control action";
|
||||
@@ -3253,6 +3395,39 @@ private:
|
||||
};
|
||||
#endif
|
||||
|
||||
// fill slot.inp_embd with the next rows of the audio stream
|
||||
// returns false if the slot has nothing to decode now
|
||||
bool slot_audio_step(server_slot & slot) {
|
||||
if (!slot.inp_embd.empty()) {
|
||||
return true; // rows from a previous step, not decoded yet
|
||||
}
|
||||
|
||||
const int32_t n = mtmd_audio_stream_step(slot.audio_stream.get(), slot.sampled);
|
||||
if (n > 0) {
|
||||
const float * rows = mtmd_audio_stream_get_output_embd(slot.audio_stream.get());
|
||||
slot.inp_embd.assign(rows, rows + (size_t) n * batch.n_embd);
|
||||
slot.audio_wait = false;
|
||||
return true;
|
||||
}
|
||||
if (n == 0) {
|
||||
slot.audio_wait = true;
|
||||
return false;
|
||||
}
|
||||
if (n == -2) {
|
||||
send_error(slot, "audio stream error", ERROR_TYPE_SERVER);
|
||||
slot.release();
|
||||
return false;
|
||||
}
|
||||
|
||||
// end of stream
|
||||
slot.stop = STOP_TYPE_EOS;
|
||||
slot.has_next_token = false;
|
||||
slot.print_timings();
|
||||
send_final_response(slot);
|
||||
slot.release();
|
||||
return false;
|
||||
}
|
||||
|
||||
void update_slots() {
|
||||
#ifdef DEBUG_TIMINGS
|
||||
static int64_t t_prev = 0;
|
||||
@@ -3268,11 +3443,12 @@ private:
|
||||
#endif
|
||||
|
||||
// check if all slots are idle
|
||||
// a realtime slot waiting for audio has no work, the next audio control task wakes the loop
|
||||
{
|
||||
bool all_idle = true;
|
||||
|
||||
for (auto & slot : slots) {
|
||||
if (slot.is_processing()) {
|
||||
if (slot.is_processing() && !slot.audio_wait) {
|
||||
all_idle = false;
|
||||
break;
|
||||
}
|
||||
@@ -3305,6 +3481,14 @@ private:
|
||||
return;
|
||||
}
|
||||
|
||||
// note: TTS slots bypass the shared batch entirely
|
||||
try {
|
||||
process_tts_slots();
|
||||
} catch (const std::exception & e) {
|
||||
SRV_ERR("process_tts_slots() failed: %s\n", e.what());
|
||||
abort_all_slots("process_tts_slots() failed: " + std::string(e.what()));
|
||||
}
|
||||
|
||||
GGML_ASSERT(batch.slot_batched || batch.size() == 0);
|
||||
|
||||
if (batch.slot_batched) {
|
||||
@@ -3367,10 +3551,89 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
void process_tts_slots() {
|
||||
iterate(slots, [&](server_slot & slot) {
|
||||
if (!slot.is_processing() || slot.task->type != SERVER_TASK_TYPE_TTS) {
|
||||
return;
|
||||
}
|
||||
|
||||
llama_set_embeddings(ctx_tgt, true);
|
||||
|
||||
if (slot.state == SLOT_STATE_PROCESSING_PROMPT) {
|
||||
const int32_t ret = slot.tts.ctx.step_prompt(llama_n_batch(ctx_tgt));
|
||||
if (ret < 0) {
|
||||
send_error(slot, "TTS prompt processing failed", ERROR_TYPE_SERVER);
|
||||
slot.release();
|
||||
} else if (ret == 0) {
|
||||
// done prompt, do sample and save h_state_prompt
|
||||
slot.tts.sampled = common_sampler_sample(slot.smpl.get(), ctx_tgt, -1);
|
||||
common_sampler_accept(slot.smpl.get(), slot.tts.sampled, true);
|
||||
const float * h_embd = llama_get_embeddings_ith(ctx_tgt, -1);
|
||||
slot.tts.h_state_prompt.assign(h_embd, h_embd + llama_model_n_embd(model_tgt));
|
||||
slot.tts.h_state = slot.tts.h_state_prompt.data();
|
||||
slot.state = SLOT_STATE_GENERATING;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const int32_t n_predict = slot.n_predict_max > 0 ? slot.n_predict_max : 512;
|
||||
if (slot.tts.stop || slot.tts.n_decoded >= n_predict) {
|
||||
int32_t sample_rate = 0;
|
||||
const char * data = nullptr;
|
||||
size_t data_len = 0;
|
||||
// generation truly ends here: force out any sub-window remainder still buffered
|
||||
if (slot.tts.ctx.flush() != 0 || slot.tts.ctx.get_output(&sample_rate, &data, &data_len) != 0) {
|
||||
send_error(slot, "failed to finalize TTS output", ERROR_TYPE_SERVER);
|
||||
} else {
|
||||
send_tts_result(slot, sample_rate, data, data_len, true);
|
||||
}
|
||||
slot.release();
|
||||
return;
|
||||
}
|
||||
|
||||
const float * h_state_next = nullptr;
|
||||
bool stop = false;
|
||||
if (slot.tts.ctx.step_gen(slot.tts.sampled, slot.tts.h_state, &h_state_next, &stop) != 0) {
|
||||
send_error(slot, "TTS generation failed", ERROR_TYPE_SERVER);
|
||||
slot.release();
|
||||
return;
|
||||
}
|
||||
if (h_state_next == nullptr) {
|
||||
// end-of-speech without a new frame (e.g. pocket-tts eos head)
|
||||
slot.tts.stop = true;
|
||||
return;
|
||||
}
|
||||
slot.tts.h_state = h_state_next;
|
||||
slot.tts.n_decoded++;
|
||||
slot.tts.stop = stop;
|
||||
|
||||
if (!stop) {
|
||||
slot.tts.sampled = common_sampler_sample(slot.smpl.get(), ctx_tgt, -1);
|
||||
common_sampler_accept(slot.smpl.get(), slot.tts.sampled, true);
|
||||
}
|
||||
|
||||
if (slot.task->params.stream) {
|
||||
int32_t sample_rate = 0;
|
||||
const char * data = nullptr;
|
||||
size_t data_len = 0;
|
||||
if (slot.tts.ctx.get_output(&sample_rate, &data, &data_len) != 0) {
|
||||
send_error(slot, "TTS streaming output failed", ERROR_TYPE_SERVER);
|
||||
slot.release();
|
||||
} else if (data_len > 0) {
|
||||
send_tts_result(slot, sample_rate, data, data_len, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void pre_decode() {
|
||||
// apply context-shift if needed
|
||||
// TODO: simplify and improve
|
||||
iterate(slots, [&](server_slot & slot) {
|
||||
if (slot.task && slot.task->type == SERVER_TASK_TYPE_TTS) {
|
||||
// TTS slots drive their own decode loop in process_tts_slots(), never enter the shared batch
|
||||
return;
|
||||
}
|
||||
if (slot.state == SLOT_STATE_GENERATING && slot.prompt.n_tokens() + 1 >= slot.n_ctx) {
|
||||
if (!params_base.ctx_shift) {
|
||||
// this check is redundant (for good)
|
||||
@@ -3443,10 +3706,14 @@ private:
|
||||
|
||||
// determine which slots are generating and drafting
|
||||
iterate(slots, [&](server_slot & slot) {
|
||||
if (slot.state != SLOT_STATE_GENERATING) {
|
||||
if (slot.state != SLOT_STATE_GENERATING || slot.task->type == SERVER_TASK_TYPE_TTS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (slot.audio_stream && !slot_audio_step(slot)) {
|
||||
return; // waiting for audio, or the stream ended
|
||||
}
|
||||
|
||||
// check if we can batch this slot with the previous one
|
||||
if (!slot_batched) {
|
||||
slot_batched = &slot;
|
||||
@@ -3585,7 +3852,7 @@ private:
|
||||
return; // batch is full, skip remaining slots
|
||||
}
|
||||
|
||||
if (!slot.is_processing()) {
|
||||
if (!slot.is_processing() || slot.task->type == SERVER_TASK_TYPE_TTS) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3600,6 +3867,11 @@ private:
|
||||
return;
|
||||
}
|
||||
|
||||
// no prompt, the rows come from the audio stream
|
||||
if (slot.task->type == SERVER_TASK_TYPE_TRANSCRIBE_RT) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this slot still has a prompt to be processed
|
||||
if (slot.state == SLOT_STATE_PROCESSING_PROMPT || slot.state == SLOT_STATE_STARTED) {
|
||||
const auto & input_tokens = slot.task->tokens;
|
||||
@@ -4521,11 +4793,14 @@ private:
|
||||
|
||||
GGML_ASSERT(n_draft > 0);
|
||||
|
||||
// batch indices of the draft tokens, used to get the token probabilities below
|
||||
auto spec_i_batch = std::move(slot.spec_i_batch);
|
||||
|
||||
// verify and try to accept the draft
|
||||
{
|
||||
common_sampler_ptr smpl_save(common_sampler_clone(slot.smpl.get()));
|
||||
|
||||
GGML_ASSERT(slot.spec_i_batch.size() == n_draft + 1);
|
||||
GGML_ASSERT(spec_i_batch.size() == n_draft + 1);
|
||||
GGML_ASSERT(slot.spec_draft_q.empty() || (slot.spec_draft_q.size() == slot.spec_draft.size()));
|
||||
const auto & synth_probs = common_speculative_get_synth_probs(spec.get());
|
||||
|
||||
@@ -4536,16 +4811,15 @@ private:
|
||||
if (!synth_probs.empty()) {
|
||||
// synthetic acceptance replaces verification entirely, so it comes first
|
||||
accepted = server_sample_and_accept_synth(
|
||||
slot.smpl.get(), slot.ctx_tgt, slot.spec_i_batch, slot.spec_draft,
|
||||
slot.smpl.get(), slot.ctx_tgt, spec_i_batch, slot.spec_draft,
|
||||
synth_probs, slot.spec_synth_rng, slot.spec_is_replay);
|
||||
} else if (slot.spec_is_replay && slot.use_spec_rejection()) {
|
||||
accepted = server_accept_replay(slot.smpl.get(), slot.ctx_tgt, slot.spec_i_batch, slot.spec_draft);
|
||||
accepted = server_accept_replay(slot.smpl.get(), slot.ctx_tgt, spec_i_batch, slot.spec_draft);
|
||||
} else if (use_rejection) {
|
||||
accepted = common_sampler_sample_and_accept_n_rejection(slot.smpl.get(), slot.ctx_tgt, slot.spec_i_batch, slot.spec_draft, slot.spec_draft_q);
|
||||
accepted = common_sampler_sample_and_accept_n_rejection(slot.smpl.get(), slot.ctx_tgt, spec_i_batch, slot.spec_draft, slot.spec_draft_q);
|
||||
} else {
|
||||
accepted = common_sampler_sample_and_accept_n(slot.smpl.get(), slot.ctx_tgt, slot.spec_i_batch, slot.spec_draft);
|
||||
accepted = common_sampler_sample_and_accept_n(slot.smpl.get(), slot.ctx_tgt, spec_i_batch, slot.spec_draft);
|
||||
}
|
||||
slot.spec_i_batch.clear();
|
||||
|
||||
GGML_ASSERT(accepted.size() >= 1);
|
||||
|
||||
@@ -4632,7 +4906,10 @@ private:
|
||||
result.text_to_send = common_token_to_piece(slot.ctx_tgt, result.tok, accept_special_token(slot, result.tok));
|
||||
result.prob = 1.0f; // set later
|
||||
|
||||
// TODO: set result.probs
|
||||
// post_sampling_probs is not supported with speculative decoding
|
||||
if (slot.task->params.sampling.n_probs > 0 && !slot.task->params.post_sampling_probs) {
|
||||
populate_token_probs(slot, result, false, params_base.special, spec_i_batch[i]);
|
||||
}
|
||||
|
||||
slot.stats.n_gen += 1;
|
||||
|
||||
@@ -4820,11 +5097,14 @@ server_context_meta server_context::get_meta() const {
|
||||
/* model_aliases */ impl->model_aliases,
|
||||
/* model_tags */ impl->model_tags,
|
||||
/* model_path */ impl->params_base.model.path,
|
||||
/* model_output_modalities */ server_model_output_modalities(common_get_decision_type(impl->model_tgt)),
|
||||
/* model_output_modalities */ server_model_output_modalities(impl->model_tgt, impl->has_cap_chat(), impl->has_cap_tts()),
|
||||
/* has_mtmd */ impl->mctx != nullptr,
|
||||
/* has_inp_image */ impl->chat_params.allow_image,
|
||||
/* has_inp_audio */ impl->chat_params.allow_audio,
|
||||
/* has_inp_video */ impl->chat_params.allow_video,
|
||||
/* has_cap_chat */ impl->has_cap_chat(),
|
||||
/* has_cap_tts */ impl->has_cap_tts(),
|
||||
/* has_audio_stream */ impl->mctx != nullptr && mtmd_support_audio_stream(impl->mctx),
|
||||
/* json_ui_settings */ impl->json_ui_settings,
|
||||
/* slot_n_ctx */ impl->n_ctx_slot(),
|
||||
/* pooling_type */ llama_pooling_type(impl->ctx_tgt),
|
||||
@@ -4899,6 +5179,11 @@ std::unique_ptr<server_res_generator> server_routes::handle_completions_impl(
|
||||
|
||||
res->set_req(&req); // will also set spipe if needed
|
||||
|
||||
if (!ctx_server.has_cap_chat()) {
|
||||
res->error(format_error_response("this server does not support chat/completions", ERROR_TYPE_NOT_SUPPORTED));
|
||||
return res;
|
||||
}
|
||||
|
||||
int32_t sse_ping_interval = params.sse_ping_interval;
|
||||
|
||||
try {
|
||||
@@ -4992,10 +5277,20 @@ std::unique_ptr<server_res_generator> server_routes::handle_completions_impl(
|
||||
res->ok(arr[0]);
|
||||
} else if (res_type == TASK_RESPONSE_TYPE_OAI_CHAT || res_type == TASK_RESPONSE_TYPE_OAI_CMPL) {
|
||||
// if multiple results in OAI format, we need to re-format them
|
||||
json & usage = arr[0]["usage"];
|
||||
json & choices = arr[0]["choices"];
|
||||
for (size_t i = 1; i < arr.size(); i++) {
|
||||
choices.push_back(std::move(arr[i]["choices"][0]));
|
||||
}
|
||||
|
||||
// OAI counts generated tokens across all choices
|
||||
int64_t n_completion_tokens = 0;
|
||||
for (auto & item : arr) {
|
||||
n_completion_tokens += item["usage"]["completion_tokens"].get<int64_t>();
|
||||
}
|
||||
usage["completion_tokens"] = n_completion_tokens;
|
||||
usage["total_tokens"] = usage["prompt_tokens"].get<int64_t>() + n_completion_tokens;
|
||||
|
||||
res->ok(arr[0]);
|
||||
} else {
|
||||
// multi-results, non-OAI compat
|
||||
@@ -5605,7 +5900,7 @@ void server_routes::init_routes() {
|
||||
server_task task(SERVER_TASK_TYPE_CONTROL);
|
||||
task.id = rd.get_new_id();
|
||||
task.params.control_cmpl_id = cmpl_id;
|
||||
task.params.control_action = action;
|
||||
task.params.control_action = SERVER_CONTROL_ACTION_REASONING_END;
|
||||
rd.post_task(std::move(task));
|
||||
}
|
||||
|
||||
@@ -5680,6 +5975,197 @@ void server_routes::init_routes() {
|
||||
session);
|
||||
};
|
||||
|
||||
// realtime transcription over websocket, modeled after the OpenAI realtime API
|
||||
// audio is PCM16 mono at the model sample rate, sent as base64 in input_audio_buffer.append events
|
||||
// input_audio_buffer.commit with "final": true ends the input, closing the socket also ends it
|
||||
this->ws_realtime = [this](const server_http_req &, server_http_context::ws_conn & conn) {
|
||||
auto send_event = [&conn](json ev) {
|
||||
ev["event_id"] = "event_" + random_string();
|
||||
conn.send(safe_json_to_str(ev));
|
||||
};
|
||||
auto send_error_event = [&send_event](const std::string & message, const std::string & type) {
|
||||
send_event(json {
|
||||
{"type", "error"},
|
||||
{"error", {
|
||||
{"type", type},
|
||||
{"code", nullptr},
|
||||
{"message", message},
|
||||
{"param", nullptr},
|
||||
}},
|
||||
});
|
||||
};
|
||||
|
||||
if (!meta->has_audio_stream) {
|
||||
send_error_event("The current model does not support realtime transcription.", "invalid_request_error");
|
||||
conn.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string session_id = "sess_" + random_string();
|
||||
const json session_config = json {
|
||||
{"id", session_id},
|
||||
{"object", "realtime.transcription_session"},
|
||||
{"type", "transcription"},
|
||||
{"audio", {{"input", {
|
||||
{"format", {{"type", "audio/pcm"}, {"rate", mtmd_get_audio_sample_rate(ctx_server.mctx)}}},
|
||||
{"transcription", {{"model", meta->model_name}}},
|
||||
{"turn_detection", nullptr},
|
||||
{"noise_reduction", nullptr},
|
||||
}}}},
|
||||
};
|
||||
|
||||
auto res = create_response();
|
||||
auto & rd = res->rd;
|
||||
{
|
||||
server_task task(SERVER_TASK_TYPE_TRANSCRIBE_RT);
|
||||
task.id = rd.get_new_id();
|
||||
task.params.sampling = params.sampling;
|
||||
task.params.stream = true;
|
||||
task.params.oaicompat_cmpl_id = session_id;
|
||||
task.params.oaicompat_model = meta->model_name;
|
||||
rd.post_task(std::move(task));
|
||||
}
|
||||
|
||||
// the slot answers once its audio stream is live, the session starts there
|
||||
{
|
||||
auto result = rd.next([&conn]() { return !conn.is_open(); });
|
||||
if (!result || result->is_error()) {
|
||||
if (result) {
|
||||
send_error_event(json_value(result->to_json(), "message", std::string("server error")), "server_error");
|
||||
}
|
||||
conn.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
send_event(json {{"type", "session.created"}, {"session", session_config}});
|
||||
|
||||
// one item per committed audio buffer, deltas are attributed to the current item
|
||||
std::mutex mutex_item;
|
||||
std::string item_id = "item_" + random_string();
|
||||
std::string item_prev; // the committed item before item_id, empty for the first one
|
||||
std::string item_text;
|
||||
|
||||
// called with mutex_item held
|
||||
auto send_completed = [&]() {
|
||||
send_event(json {
|
||||
{"type", "conversation.item.input_audio_transcription.completed"},
|
||||
{"item_id", item_id},
|
||||
{"content_index", 0},
|
||||
{"transcript", item_text},
|
||||
});
|
||||
item_text.clear();
|
||||
};
|
||||
|
||||
auto post_control = [&](server_control_action action, std::vector<float> && audio) {
|
||||
server_response_reader ctl(queue_tasks, queue_results, HTTP_POLLING_SECONDS);
|
||||
server_task task(SERVER_TASK_TYPE_CONTROL);
|
||||
task.id = ctl.get_new_id();
|
||||
task.params.control_action = action;
|
||||
task.params.control_cmpl_id = session_id;
|
||||
task.params.control_audio = std::move(audio);
|
||||
ctl.post_task(std::move(task));
|
||||
auto result = ctl.next([&conn]() { return !conn.is_open(); });
|
||||
auto * control = result ? dynamic_cast<server_task_result_control *>(result.get()) : nullptr;
|
||||
return control && control->success;
|
||||
};
|
||||
|
||||
// client events are read on this thread, the transcript is sent from the thread below
|
||||
std::atomic<bool> input_ended{false};
|
||||
std::thread reader([&]() {
|
||||
std::string msg;
|
||||
while (conn.read(msg) != 0) {
|
||||
json ev;
|
||||
try {
|
||||
ev = json::parse(msg);
|
||||
} catch (const std::exception &) {
|
||||
send_error_event("invalid JSON", "invalid_request_error");
|
||||
continue;
|
||||
}
|
||||
const std::string type = json_value(ev, "type", std::string());
|
||||
if (type == "session.update" || type == "transcription_session.update") {
|
||||
send_event(json {{"type", "session.updated"}, {"session", session_config}});
|
||||
} else if (type == "input_audio_buffer.append") {
|
||||
std::string pcm;
|
||||
try {
|
||||
pcm = base64::decode(json_value(ev, "audio", std::string()));
|
||||
} catch (const std::exception &) {
|
||||
send_error_event("invalid base64 audio", "invalid_request_error");
|
||||
continue;
|
||||
}
|
||||
std::vector<float> samples(pcm.size() / 2);
|
||||
for (size_t i = 0; i < samples.size(); i++) {
|
||||
int16_t v;
|
||||
memcpy(&v, pcm.data() + 2 * i, sizeof(v));
|
||||
samples[i] = v / 32768.0f;
|
||||
}
|
||||
if (!post_control(SERVER_CONTROL_ACTION_AUDIO_APPEND, std::move(samples))) {
|
||||
break;
|
||||
}
|
||||
} else if (type == "input_audio_buffer.commit") {
|
||||
// "final": true ends the input, the last item is completed when the stream drains
|
||||
if (json_value(ev, "final", false)) {
|
||||
input_ended = true;
|
||||
post_control(SERVER_CONTROL_ACTION_AUDIO_END, {});
|
||||
continue;
|
||||
}
|
||||
if (!post_control(SERVER_CONTROL_ACTION_AUDIO_COMMIT, {})) {
|
||||
break;
|
||||
}
|
||||
// the current item is the committed one, its transcript completes it and the next item opens
|
||||
std::lock_guard<std::mutex> lock(mutex_item);
|
||||
send_event(json {
|
||||
{"type", "input_audio_buffer.committed"},
|
||||
{"previous_item_id", item_prev.empty() ? json(nullptr) : json(item_prev)},
|
||||
{"item_id", item_id},
|
||||
});
|
||||
send_completed();
|
||||
item_prev = item_id;
|
||||
item_id = "item_" + random_string();
|
||||
} else if (type == "input_audio_buffer.clear") {
|
||||
send_event(json {{"type", "input_audio_buffer.cleared"}});
|
||||
} else {
|
||||
send_error_event("unknown event type: " + type, "invalid_request_error");
|
||||
}
|
||||
}
|
||||
if (!input_ended.exchange(true)) {
|
||||
post_control(SERVER_CONTROL_ACTION_AUDIO_END, {});
|
||||
}
|
||||
// the result loop waits on the socket, nobody reads it from here on
|
||||
conn.close();
|
||||
});
|
||||
|
||||
while (true) {
|
||||
auto result = rd.next([&conn]() { return !conn.is_open(); });
|
||||
if (!result) {
|
||||
break; // connection closed
|
||||
}
|
||||
if (result->is_error()) {
|
||||
send_error_event(json_value(result->to_json(), "message", std::string("server error")), "server_error");
|
||||
break;
|
||||
}
|
||||
if (auto * partial = dynamic_cast<server_task_result_cmpl_partial *>(result.get())) {
|
||||
if (!partial->content.text.empty()) {
|
||||
std::lock_guard<std::mutex> lock(mutex_item);
|
||||
item_text += partial->content.text;
|
||||
send_event(json {
|
||||
{"type", "conversation.item.input_audio_transcription.delta"},
|
||||
{"item_id", item_id},
|
||||
{"content_index", 0},
|
||||
{"delta", partial->content.text},
|
||||
});
|
||||
}
|
||||
} else if (result->is_stop()) {
|
||||
std::lock_guard<std::mutex> lock(mutex_item);
|
||||
send_completed();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
input_ended = true;
|
||||
conn.close();
|
||||
reader.join();
|
||||
};
|
||||
|
||||
this->post_anthropic_messages = [this](const server_http_req & req) {
|
||||
auto res = create_response();
|
||||
std::vector<raw_buffer> files;
|
||||
@@ -5877,6 +6363,171 @@ void server_routes::init_routes() {
|
||||
return res;
|
||||
};
|
||||
|
||||
this->post_tts = [this](const server_http_req & req) {
|
||||
auto res = create_response();
|
||||
res->set_req(&req); // will also set spipe if needed
|
||||
|
||||
if (!ctx_server.has_cap_tts()) {
|
||||
res->error(format_error_response("this server does not support audio generation", ERROR_TYPE_NOT_SUPPORTED));
|
||||
return res;
|
||||
}
|
||||
|
||||
const auto info = mtmd_gen_audio_get_info(ctx_server.mctx);
|
||||
|
||||
json body = json::parse(req.body);
|
||||
|
||||
// multipart form fields arrive as strings, correct the types of the numeric and boolean params
|
||||
for (const char * k : { "top_k", "n_predict", "max_tokens", "seed" }) {
|
||||
if (body.contains(k) && body[k].is_string()) {
|
||||
body[k] = std::stoll(body[k].get<std::string>());
|
||||
}
|
||||
}
|
||||
for (const char * k : { "top_p", "repeat_penalty" }) {
|
||||
if (body.contains(k) && body[k].is_string()) {
|
||||
body[k] = std::stof(body[k].get<std::string>());
|
||||
}
|
||||
}
|
||||
if (body.contains("stream") && body["stream"].is_string()) {
|
||||
body["stream"] = body["stream"] == "true";
|
||||
}
|
||||
|
||||
std::string prompt = json_value(body, "input", json_value(body, "prompt", std::string()));
|
||||
if (prompt.empty()) {
|
||||
res->error(format_error_response("\"input\" must be a non-empty string", ERROR_TYPE_INVALID_REQUEST));
|
||||
return res;
|
||||
}
|
||||
|
||||
const std::string response_format = json_value(body, "response_format", std::string("wav"));
|
||||
|
||||
server_task task(SERVER_TASK_TYPE_TTS);
|
||||
server_schema::eval_tts_schema(params, task, body);
|
||||
const bool stream = task.params.stream;
|
||||
task.tts_inp.set_prompt(prompt);
|
||||
task.tts_inp.set_lang(json_value(body, "lang", std::string()));
|
||||
task.tts_inp.data.stream = stream;
|
||||
task.tts_inp.data.out_type = response_format == "pcm"
|
||||
? MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM
|
||||
: MTMD_HELPER_GEN_AUDIO_OUTTYPE_WAV;
|
||||
// note: -1 is clamped to 0 by the sampler, it will disable the penalty
|
||||
task.params.sampling.penalty_last_n = task.params.n_predict > 0 ? task.params.n_predict : 512;
|
||||
task.params.sampling.seed = task.tts_inp.data.seed; // same seed for the codec/vocoder RNG
|
||||
if (task.tts_inp.data.top_k > 0) {
|
||||
task.params.sampling.top_k = task.tts_inp.data.top_k;
|
||||
}
|
||||
if (task.tts_inp.data.top_p > 0) {
|
||||
task.params.sampling.top_p = task.tts_inp.data.top_p;
|
||||
}
|
||||
|
||||
// speaker reference: either an uploaded form file ("speaker_ref") or a base64 JSON field ("speaker_ref_b64")
|
||||
const unsigned char * speaker_ref_data = nullptr;
|
||||
size_t speaker_ref_len = 0;
|
||||
std::string speaker_ref_b64_decoded;
|
||||
|
||||
auto speaker_ref_file = req.files.find("speaker_ref");
|
||||
if (speaker_ref_file != req.files.end()) {
|
||||
speaker_ref_data = speaker_ref_file->second.data.data();
|
||||
speaker_ref_len = speaker_ref_file->second.data.size();
|
||||
} else {
|
||||
std::string speaker_ref_b64 = json_value(body, "speaker_ref_b64", std::string());
|
||||
if (!speaker_ref_b64.empty()) {
|
||||
try {
|
||||
speaker_ref_b64_decoded = base64::decode(speaker_ref_b64);
|
||||
} catch (const std::exception &) {
|
||||
res->error(format_error_response("\"speaker_ref_b64\" is not valid base64", ERROR_TYPE_INVALID_REQUEST));
|
||||
return res;
|
||||
}
|
||||
speaker_ref_data = (const unsigned char *) speaker_ref_b64_decoded.data();
|
||||
speaker_ref_len = speaker_ref_b64_decoded.size();
|
||||
}
|
||||
}
|
||||
|
||||
if (speaker_ref_len > 0) {
|
||||
auto wrapper = mtmd_helper_bitmap_init_from_buf(ctx_server.mctx, speaker_ref_data, speaker_ref_len, false, ctx_server.init_opt);
|
||||
if (!wrapper.bitmap) {
|
||||
res->error(format_error_response("failed to decode \"speaker_ref\"", ERROR_TYPE_INVALID_REQUEST));
|
||||
return res;
|
||||
}
|
||||
task.tts_inp.set_speaker_ref(mtmd::bitmap_ptr(wrapper.bitmap));
|
||||
} else {
|
||||
SRV_WRN("%s", "no speaker reference provided, the model may behave randomly\n");
|
||||
}
|
||||
|
||||
auto & rd = res->rd;
|
||||
task.id = rd.get_new_id();
|
||||
rd.post_task(std::move(task));
|
||||
|
||||
// raw float32 LE mono samples; audio/L16 would imply 16-bit big-endian (RFC 2586)
|
||||
const std::string content_type = response_format == "pcm"
|
||||
? "audio/pcm;rate=" + std::to_string(info.sample_rate) + ";encoding=float;bits=32"
|
||||
: "audio/wav";
|
||||
|
||||
if (!stream) {
|
||||
auto result = rd.next(req.should_stop);
|
||||
if (!result) {
|
||||
GGML_ASSERT(req.should_stop());
|
||||
return res; // connection is closed
|
||||
}
|
||||
if (result->is_error()) {
|
||||
res->error(result->to_json());
|
||||
return res;
|
||||
}
|
||||
auto * tts_res = dynamic_cast<server_task_result_tts *>(result.get());
|
||||
GGML_ASSERT(tts_res != nullptr);
|
||||
res->status = 200;
|
||||
res->content_type = content_type;
|
||||
res->data = std::move(tts_res->audio);
|
||||
return res;
|
||||
} else {
|
||||
auto first_result = rd.next(req.should_stop);
|
||||
if (!first_result) {
|
||||
GGML_ASSERT(req.should_stop());
|
||||
return res; // connection is closed
|
||||
}
|
||||
if (first_result->is_error()) {
|
||||
res->error(first_result->to_json());
|
||||
return res;
|
||||
}
|
||||
auto * first_tts_res = dynamic_cast<server_task_result_tts *>(first_result.get());
|
||||
GGML_ASSERT(first_tts_res != nullptr);
|
||||
|
||||
res->status = 200;
|
||||
res->content_type = content_type;
|
||||
res->data = std::move(first_tts_res->audio);
|
||||
bool is_done = first_tts_res->final;
|
||||
|
||||
res->set_next([res_this = res.get(), is_done](std::string & output) mutable -> bool {
|
||||
// flush buffered audio before is_done - the first result can already be final
|
||||
if (!res_this->data.empty()) {
|
||||
output = std::move(res_this->data);
|
||||
res_this->data.clear();
|
||||
return true;
|
||||
}
|
||||
if (is_done) {
|
||||
return false;
|
||||
}
|
||||
if (res_this->should_stop()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
server_response_reader & rd = res_this->rd;
|
||||
if (!rd.has_next()) {
|
||||
return false;
|
||||
}
|
||||
auto result = rd.next([&res_this]() { return res_this->should_stop(); });
|
||||
if (!result || result->is_error()) {
|
||||
return false;
|
||||
}
|
||||
auto * tts_res = dynamic_cast<server_task_result_tts *>(result.get());
|
||||
GGML_ASSERT(tts_res != nullptr);
|
||||
output = std::move(tts_res->audio);
|
||||
is_done = tts_res->final;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
this->post_systemone = [this](const server_http_req & req) {
|
||||
auto res = create_response();
|
||||
const auto & decision = ctx_server.decision;
|
||||
@@ -5889,7 +6540,7 @@ void server_routes::init_routes() {
|
||||
const auto questions = decision.parse_questions(body);
|
||||
|
||||
std::vector<raw_buffer> files;
|
||||
const json state = decision.parse_state(body, files);
|
||||
const json state = decision.prepare_state(decision.parse_state(body, files));
|
||||
if (!files.empty() && (!decision.can_use_images() || !meta->has_inp_image)) {
|
||||
res->error(format_error_response("This server does not support image input for decisions. For a model that supports it, start it with `--mmproj`", ERROR_TYPE_NOT_SUPPORTED));
|
||||
return res;
|
||||
|
||||
@@ -24,6 +24,9 @@ struct server_context_meta {
|
||||
bool has_inp_image;
|
||||
bool has_inp_audio;
|
||||
bool has_inp_video;
|
||||
bool has_cap_chat;
|
||||
bool has_cap_tts;
|
||||
bool has_audio_stream; // realtime transcription, see mtmd_support_audio_stream()
|
||||
json json_ui_settings;
|
||||
int slot_n_ctx;
|
||||
enum llama_pooling_type pooling_type;
|
||||
@@ -144,6 +147,7 @@ struct server_routes {
|
||||
server_http_context::handler_t post_responses_oai;
|
||||
server_http_context::handler_t post_responses_tok_oai;
|
||||
server_http_context::handler_t post_transcriptions_oai;
|
||||
server_http_context::ws_handler_t ws_realtime;
|
||||
server_http_context::handler_t post_anthropic_messages;
|
||||
server_http_context::handler_t post_anthropic_count_tokens;
|
||||
server_http_context::handler_t post_apply_template;
|
||||
@@ -153,6 +157,7 @@ struct server_routes {
|
||||
server_http_context::handler_t post_embeddings;
|
||||
server_http_context::handler_t post_embeddings_oai;
|
||||
server_http_context::handler_t post_rerank;
|
||||
server_http_context::handler_t post_tts;
|
||||
server_http_context::handler_t post_systemone;
|
||||
server_http_context::handler_t get_lora_adapters;
|
||||
server_http_context::handler_t post_lora_adapters;
|
||||
|
||||
@@ -19,6 +19,10 @@ static const char * decision_question_type_name(server_decision_question_type ty
|
||||
// lev reads noul from a rating scale: 0 = certainly no, 8 = certainly yes
|
||||
static const size_t DECISION_LEV_N_RATINGS = 9;
|
||||
|
||||
// h2o-lightning follows serve_config.json and h2o_lightning_shim.py of the model repo
|
||||
// a longer state keeps its head and its tail
|
||||
static const size_t DECISION_H2O_MAX_STATE_TOKENS = 32000;
|
||||
|
||||
static std::string decision_meta_str(const llama_model * model, const std::string & key) {
|
||||
char buf[256];
|
||||
const int32_t n = llama_model_meta_val_str(model, key.c_str(), buf, sizeof(buf));
|
||||
@@ -164,6 +168,39 @@ void server_decision_context::init(const llama_model * model) {
|
||||
throw std::runtime_error("decision model has no mask token");
|
||||
}
|
||||
n_options_max = 255;
|
||||
} else if (model_type == COMMON_DECISION_TYPE_H2O_LIGHTNING) {
|
||||
// the answer is read at the token of a label, its text is given to the template
|
||||
for (uint32_t i = 0; llama_model_decision_label(model, i) != nullptr; i++) {
|
||||
const std::string text = llama_model_decision_label(model, i);
|
||||
const auto toks = common_tokenize(vocab, text, false, false);
|
||||
if (toks.size() != 1 || std::find(labels.begin(), labels.end(), toks[0]) != labels.end()) {
|
||||
throw std::runtime_error(string_format("decision label \"%s\" is not a single distinct token", text.c_str()));
|
||||
}
|
||||
labels.push_back(toks[0]);
|
||||
label_texts.push_back(text);
|
||||
}
|
||||
if (labels.empty()) {
|
||||
throw std::runtime_error("decision model has no labels");
|
||||
}
|
||||
// the texts of the control and user-defined tokens are quoted in the input, so that they stay text
|
||||
for (llama_token id = 0; id < llama_vocab_n_tokens(vocab); id++) {
|
||||
if (llama_vocab_get_attr(vocab, id) & (LLAMA_TOKEN_ATTR_CONTROL | LLAMA_TOKEN_ATTR_USER_DEFINED)) {
|
||||
const std::string text = llama_vocab_get_text(vocab, id);
|
||||
if (!text.empty()) {
|
||||
quoted_spellings.push_back(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::stable_sort(quoted_spellings.begin(), quoted_spellings.end(), [](const std::string & a, const std::string & b) {
|
||||
return a.size() > b.size();
|
||||
});
|
||||
for (const auto & spelling : quoted_spellings) {
|
||||
if (quoted_first.find(spelling[0]) == std::string::npos) {
|
||||
quoted_first += spelling[0];
|
||||
}
|
||||
}
|
||||
n_options_max = labels.size();
|
||||
noul_true_first = true;
|
||||
} else {
|
||||
throw std::runtime_error("unsupported decision model type: " + type_name);
|
||||
}
|
||||
@@ -201,8 +238,10 @@ std::vector<server_decision_question> server_decision_context::parse_questions(c
|
||||
throw err("must be an object");
|
||||
}
|
||||
// the vllm-d3 prompt has a default question
|
||||
const json instructions = q.contains("instructions") ? q.at("instructions") : json();
|
||||
if (instructions.is_null() && type != COMMON_DECISION_TYPE_VLLM_D3) {
|
||||
// h2o-lightning: the instructions can be left out, the template has a default, and the type defaults to choice
|
||||
const bool is_h2o = type == COMMON_DECISION_TYPE_H2O_LIGHTNING;
|
||||
const json instructions = q.contains("instructions") ? q.at("instructions") : (is_h2o ? json(std::string()) : json());
|
||||
if (instructions.is_null() && type != COMMON_DECISION_TYPE_VLLM_D3 && !is_h2o) {
|
||||
throw err("\"instructions\" must be provided");
|
||||
}
|
||||
if (strict_payload && (!decision_is_payload(instructions) || instructions == "")) {
|
||||
@@ -213,7 +252,7 @@ std::vector<server_decision_question> server_decision_context::parse_questions(c
|
||||
question.id = id;
|
||||
question.instructions = instructions;
|
||||
|
||||
const std::string type_name = json_value(q, "type", std::string());
|
||||
const std::string type_name = json_value(q, "type", std::string(is_h2o ? "choice" : ""));
|
||||
const json criteria = q.contains("criteria") ? q.at("criteria") : json();
|
||||
|
||||
if (type_name == "choice") {
|
||||
@@ -237,7 +276,11 @@ std::vector<server_decision_question> server_decision_context::parse_questions(c
|
||||
}
|
||||
} else if (type_name == "score") {
|
||||
question.type = SERVER_DECISION_QUESTION_SCORE;
|
||||
if (!criteria.is_array() || criteria.size() < 2 || criteria.size() > 10) {
|
||||
// h2o-lightning: from 1 level, the maximum is n_options_max
|
||||
if (is_h2o && (!criteria.is_array() || criteria.empty())) {
|
||||
throw err("\"criteria\" must be a non-empty array");
|
||||
}
|
||||
if (!is_h2o && (!criteria.is_array() || criteria.size() < 2 || criteria.size() > 10)) {
|
||||
throw err("\"criteria\" must be an array of 2 to 10 levels");
|
||||
}
|
||||
for (size_t i = 0; i < criteria.size(); i++) {
|
||||
@@ -262,8 +305,8 @@ std::vector<server_decision_question> server_decision_context::parse_questions(c
|
||||
json description;
|
||||
if (criteria.is_object() && criteria.contains(key)) {
|
||||
description = criteria.at(key);
|
||||
} else if (criteria.is_object() && type == COMMON_DECISION_TYPE_LFM2_D1_OMNI) {
|
||||
// lfm2-d1-omni also reads the descriptions under "no" and "yes"
|
||||
} else if (criteria.is_object() && (type == COMMON_DECISION_TYPE_LFM2_D1_OMNI || is_h2o)) {
|
||||
// lfm2-d1-omni and h2o-lightning also read the descriptions under "no" and "yes"
|
||||
const char * alias = std::string(key) == "true" ? "yes" : "no";
|
||||
description = criteria.contains(alias) ? criteria.at(alias) : json();
|
||||
}
|
||||
@@ -506,6 +549,280 @@ static json decision_d1omni_escape(const json & val) {
|
||||
return val;
|
||||
}
|
||||
|
||||
//
|
||||
// h2o-lightning: the texts are rendered the way the reference shim renders them with Python (json.dumps, str.lstrip)
|
||||
//
|
||||
|
||||
// one code point of UTF-8 text at s[i], i moves past it; an invalid byte is read as is
|
||||
static uint32_t decision_utf8_next(const std::string & s, size_t & i) {
|
||||
const unsigned char c = s[i];
|
||||
size_t n = c < 0x80 ? 1 : (c >> 5) == 0x6 ? 2 : (c >> 4) == 0xe ? 3 : (c >> 3) == 0x1e ? 4 : 1;
|
||||
if (i + n > s.size()) {
|
||||
n = 1;
|
||||
}
|
||||
uint32_t cp = n == 1 ? c : n == 2 ? (c & 0x1f) : n == 3 ? (c & 0x0f) : (c & 0x07);
|
||||
for (size_t k = 1; k < n; k++) {
|
||||
cp = (cp << 6) | (s[i + k] & 0x3f);
|
||||
}
|
||||
i += n;
|
||||
return cp;
|
||||
}
|
||||
|
||||
// str.isspace() of Python
|
||||
static bool decision_py_isspace(uint32_t cp) {
|
||||
return (cp >= 0x09 && cp <= 0x0d) || (cp >= 0x1c && cp <= 0x20) || cp == 0x85 || cp == 0xa0 || cp == 0x1680 ||
|
||||
(cp >= 0x2000 && cp <= 0x200a) || cp == 0x2028 || cp == 0x2029 || cp == 0x202f || cp == 0x205f || cp == 0x3000;
|
||||
}
|
||||
|
||||
static std::string decision_py_rstrip(const std::string & s) {
|
||||
size_t end = 0;
|
||||
for (size_t i = 0; i < s.size();) {
|
||||
if (!decision_py_isspace(decision_utf8_next(s, i))) {
|
||||
end = i;
|
||||
}
|
||||
}
|
||||
return s.substr(0, end);
|
||||
}
|
||||
|
||||
static std::string decision_py_lstrip(const std::string & s) {
|
||||
size_t i = 0;
|
||||
while (i < s.size()) {
|
||||
size_t next = i;
|
||||
if (!decision_py_isspace(decision_utf8_next(s, next))) {
|
||||
break;
|
||||
}
|
||||
i = next;
|
||||
}
|
||||
return s.substr(i);
|
||||
}
|
||||
|
||||
// a code point as JSON escapes, a surrogate pair above the BMP
|
||||
static std::string decision_py_escape_cp(uint32_t cp) {
|
||||
if (cp < 0x10000) {
|
||||
return string_format("\\u%04x", cp);
|
||||
}
|
||||
cp -= 0x10000;
|
||||
return string_format("\\u%04x\\u%04x", 0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff));
|
||||
}
|
||||
|
||||
// repr() of a Python float: the shortest digits that read back the same, in exponent form below 1e-4 and from 1e16
|
||||
static std::string decision_py_float(double v) {
|
||||
char buf[64];
|
||||
// the fewest digits that read back the same
|
||||
for (int prec = 0; prec < 17; prec++) {
|
||||
snprintf(buf, sizeof(buf), "%.*e", prec, v);
|
||||
if (std::strtod(buf, nullptr) == v) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const std::string sci(buf); // for example "-1.25e+03"
|
||||
const size_t pos_e = sci.find('e');
|
||||
const int exp = std::atoi(sci.c_str() + pos_e + 1);
|
||||
std::string sign;
|
||||
std::string digits;
|
||||
for (size_t i = 0; i < pos_e; i++) {
|
||||
if (sci[i] == '-') {
|
||||
sign = "-";
|
||||
} else if (sci[i] != '.') {
|
||||
digits += sci[i];
|
||||
}
|
||||
}
|
||||
const int n = (int) digits.size();
|
||||
if (exp < -4 || exp >= 16) {
|
||||
return sign + digits.substr(0, 1) + (n > 1 ? "." + digits.substr(1) : "") + string_format("e%c%02d", exp < 0 ? '-' : '+', std::abs(exp));
|
||||
}
|
||||
if (exp >= n - 1) {
|
||||
return sign + digits + std::string(exp - (n - 1), '0') + ".0";
|
||||
}
|
||||
if (exp >= 0) {
|
||||
return sign + digits.substr(0, exp + 1) + "." + digits.substr(exp + 1);
|
||||
}
|
||||
return sign + "0." + std::string(-exp - 1, '0') + digits;
|
||||
}
|
||||
|
||||
static void decision_py_str(std::string & out, const std::string & s, bool ensure_ascii) {
|
||||
out += '"';
|
||||
for (size_t i = 0; i < s.size();) {
|
||||
const unsigned char c = s[i];
|
||||
switch (c) {
|
||||
case '"': out += "\\\""; i++; continue;
|
||||
case '\\': out += "\\\\"; i++; continue;
|
||||
case '\b': out += "\\b"; i++; continue;
|
||||
case '\f': out += "\\f"; i++; continue;
|
||||
case '\n': out += "\\n"; i++; continue;
|
||||
case '\r': out += "\\r"; i++; continue;
|
||||
case '\t': out += "\\t"; i++; continue;
|
||||
default: break;
|
||||
}
|
||||
if (c < 0x20 || (ensure_ascii && c == 0x7f)) {
|
||||
out += string_format("\\u%04x", c);
|
||||
i++;
|
||||
} else if (c < 0x80 || !ensure_ascii) {
|
||||
out += (char) c;
|
||||
i++;
|
||||
} else {
|
||||
out += decision_py_escape_cp(decision_utf8_next(s, i));
|
||||
}
|
||||
}
|
||||
out += '"';
|
||||
}
|
||||
|
||||
// json.dumps() of Python: indent < 0 is no indent, keys stay in their order
|
||||
static void decision_py_dump(std::string & out, const json & val, int indent, int level, const std::string & item_sep, const std::string & key_sep, bool ensure_ascii) {
|
||||
const auto newline = [&](int lvl) {
|
||||
return indent < 0 ? std::string() : "\n" + std::string(indent * lvl, ' ');
|
||||
};
|
||||
if (val.is_null()) {
|
||||
out += "null";
|
||||
} else if (val.is_boolean()) {
|
||||
out += val.get<bool>() ? "true" : "false";
|
||||
} else if (val.is_number_float()) {
|
||||
out += decision_py_float(val.get<double>());
|
||||
} else if (val.is_number()) {
|
||||
out += val.dump();
|
||||
} else if (val.is_string()) {
|
||||
decision_py_str(out, val.get<std::string>(), ensure_ascii);
|
||||
} else if (val.is_array()) {
|
||||
if (val.empty()) {
|
||||
out += "[]";
|
||||
return;
|
||||
}
|
||||
out += "[" + newline(level + 1);
|
||||
for (size_t i = 0; i < val.size(); i++) {
|
||||
if (i > 0) {
|
||||
out += item_sep + newline(level + 1);
|
||||
}
|
||||
decision_py_dump(out, val.at(i), indent, level + 1, item_sep, key_sep, ensure_ascii);
|
||||
}
|
||||
out += newline(level) + "]";
|
||||
} else if (val.is_object()) {
|
||||
if (val.empty()) {
|
||||
out += "{}";
|
||||
return;
|
||||
}
|
||||
out += "{" + newline(level + 1);
|
||||
bool first = true;
|
||||
for (const auto & [key, item] : val.items()) {
|
||||
if (!first) {
|
||||
out += item_sep + newline(level + 1);
|
||||
}
|
||||
first = false;
|
||||
decision_py_str(out, key, ensure_ascii);
|
||||
out += key_sep;
|
||||
decision_py_dump(out, item, indent, level + 1, item_sep, key_sep, ensure_ascii);
|
||||
}
|
||||
out += newline(level) + "}";
|
||||
}
|
||||
}
|
||||
|
||||
static std::string decision_py_dumps(const json & val, int indent, const std::string & item_sep, const std::string & key_sep, bool ensure_ascii) {
|
||||
std::string out;
|
||||
decision_py_dump(out, val, indent, 0, item_sep, key_sep, ensure_ascii);
|
||||
return out;
|
||||
}
|
||||
|
||||
// a state longer than max_state_tokens keeps its head and its tail around a "..." line
|
||||
std::string server_decision_context::h2o_cut_state(const std::string & text) const {
|
||||
// a byte-level BPE never has more tokens than bytes
|
||||
const size_t max_state_tokens = DECISION_H2O_MAX_STATE_TOKENS;
|
||||
if (text.size() <= max_state_tokens) {
|
||||
return text;
|
||||
}
|
||||
const llama_tokens tokens = common_tokenize(vocab, text, false, true);
|
||||
if (tokens.size() <= max_state_tokens) {
|
||||
return text;
|
||||
}
|
||||
// each half is the text of its token pieces, a cut inside a character gives U+FFFD as in a JSON response of the server
|
||||
const size_t half = max_state_tokens / 2;
|
||||
auto text_of = [&](llama_tokens::const_iterator begin, llama_tokens::const_iterator end) {
|
||||
std::string out;
|
||||
for (auto it = begin; it != end; ++it) {
|
||||
out += common_token_to_piece(vocab, *it, true);
|
||||
}
|
||||
return json::parse(json::make(out).dump_safe()).get<std::string>();
|
||||
};
|
||||
return text_of(tokens.begin(), tokens.begin() + half) + "\n...\n" + text_of(tokens.end() - half, tokens.end());
|
||||
}
|
||||
|
||||
// the special token texts written in the input are quoted with JSON escapes, so they stay text
|
||||
std::string server_decision_context::h2o_quote(const std::string & text) const {
|
||||
std::string out;
|
||||
for (size_t i = 0; i < text.size();) {
|
||||
bool is_quoted = false;
|
||||
if (quoted_first.find(text[i]) == std::string::npos) {
|
||||
out += text[i++];
|
||||
continue;
|
||||
}
|
||||
for (const auto & spelling : quoted_spellings) {
|
||||
if (text.compare(i, spelling.size(), spelling) == 0) {
|
||||
std::string quoted = spelling;
|
||||
string_replace_all(quoted, "<", "\\u003c");
|
||||
string_replace_all(quoted, ">", "\\u003e");
|
||||
out += quoted;
|
||||
i += spelling.size();
|
||||
is_quoted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!is_quoted) {
|
||||
out += text[i++];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
json server_decision_context::prepare_state(const json & state) const {
|
||||
if (type != COMMON_DECISION_TYPE_H2O_LIGHTNING) {
|
||||
return state;
|
||||
}
|
||||
// done once per request: compact JSON, the cut, the quoting
|
||||
return h2o_quote(h2o_cut_state(state.is_string() ? state.get<std::string>() : decision_py_dumps(state, -1, ",", ":", false)));
|
||||
}
|
||||
|
||||
json server_decision_context::render_h2o(const json & state, const server_decision_question & question) const {
|
||||
// the instructions are JSON with an indent of 1, the descriptions are default JSON, the state is from prepare_state()
|
||||
const std::string instructions = decision_py_lstrip(question.instructions.is_string() ?
|
||||
question.instructions.get<std::string>() : decision_py_dumps(question.instructions, 1, ",", ": ", true));
|
||||
|
||||
json options = json::array();
|
||||
for (size_t i = 0; i < question.options.size(); i++) {
|
||||
const auto & opt = question.options[i];
|
||||
json description; // null if not provided, the template has a default, except a score level that is shown as JSON null
|
||||
if (question.type == SERVER_DECISION_QUESTION_SCORE && opt.description.is_null()) {
|
||||
description = "null";
|
||||
} else if (!opt.description.is_null()) {
|
||||
description = h2o_quote(opt.description.is_string() ? opt.description.get<std::string>() : decision_py_dumps(opt.description, -1, ", ", ": ", false));
|
||||
}
|
||||
const std::string key = h2o_quote(opt.key);
|
||||
|
||||
// the user turn is stripped like Python's str.strip(), the template only trims ASCII whitespace
|
||||
// so the text that ends the turn is stripped here, a space keeps a description that is only whitespace
|
||||
if (i + 1 == question.options.size()) {
|
||||
const bool is_key = question.type == SERVER_DECISION_QUESTION_CHOICE && description.is_null();
|
||||
if (is_key || description.is_string()) {
|
||||
const std::string text = is_key ? key : description.get<std::string>();
|
||||
const std::string stripped = decision_py_rstrip(text);
|
||||
if (stripped != text) {
|
||||
description = stripped.empty() ? " " : stripped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json option = json::object();
|
||||
option["label"] = label_texts[i];
|
||||
option["key"] = key;
|
||||
option["description"] = description;
|
||||
options.push_back(option);
|
||||
}
|
||||
|
||||
json inp = json::object();
|
||||
inp["type"] = decision_question_type_name(question.type);
|
||||
inp["state"] = state;
|
||||
inp["instructions"] = h2o_quote(instructions);
|
||||
inp["options"] = options;
|
||||
return inp;
|
||||
}
|
||||
|
||||
// given to the lfm2-d1-omni template: text between the pieces of the prompt, and at the start of the pieces that are cut to a token budget
|
||||
static const std::string D1OMNI_MARKER = "<<d1omni:";
|
||||
static const std::string D1OMNI_SEP = "<<d1omni:sep>>";
|
||||
@@ -672,7 +989,8 @@ std::string server_decision_context::render(
|
||||
size_t n_images,
|
||||
bool is_audio) const {
|
||||
// the template is given raw JSON values, it serializes the ones that are not strings
|
||||
json inp = json{
|
||||
// h2o-lightning: the template is given texts
|
||||
json inp = type == COMMON_DECISION_TYPE_H2O_LIGHTNING ? render_h2o(state, question) : json{
|
||||
{"id", question.id},
|
||||
{"type", decision_question_type_name(question.type)},
|
||||
{"instructions", question.instructions},
|
||||
@@ -756,7 +1074,8 @@ void server_decision_context::fill_task(
|
||||
|
||||
const std::string prompt = render(state, questions, question, variant, files.size());
|
||||
|
||||
if (type == COMMON_DECISION_TYPE_OPENJEV || type == COMMON_DECISION_TYPE_GRANITE_SWITCH || type == COMMON_DECISION_TYPE_LEV || type == COMMON_DECISION_TYPE_NIMBLE || type == COMMON_DECISION_TYPE_PPLX_DECIDER || type == COMMON_DECISION_TYPE_VLLM_D3) {
|
||||
if (type == COMMON_DECISION_TYPE_OPENJEV || type == COMMON_DECISION_TYPE_GRANITE_SWITCH || type == COMMON_DECISION_TYPE_LEV || type == COMMON_DECISION_TYPE_NIMBLE || type == COMMON_DECISION_TYPE_PPLX_DECIDER || type == COMMON_DECISION_TYPE_VLLM_D3 ||
|
||||
type == COMMON_DECISION_TYPE_H2O_LIGHTNING) {
|
||||
// lev reads the ratings of a noul question at its first labels, not at the digits
|
||||
task.decision.labels.assign(labels.begin(), labels.begin() + n_outputs(question));
|
||||
}
|
||||
@@ -769,7 +1088,8 @@ void server_decision_context::fill_task(
|
||||
task.decision.label_groups.push_back(group.size());
|
||||
}
|
||||
}
|
||||
if (type == COMMON_DECISION_TYPE_OPENJEV || type == COMMON_DECISION_TYPE_LEV || type == COMMON_DECISION_TYPE_NIMBLE || type == COMMON_DECISION_TYPE_PPLX_DECIDER || type == COMMON_DECISION_TYPE_VLLM_D3 || type == COMMON_DECISION_TYPE_LFM2_D1) {
|
||||
if (type == COMMON_DECISION_TYPE_OPENJEV || type == COMMON_DECISION_TYPE_LEV || type == COMMON_DECISION_TYPE_NIMBLE || type == COMMON_DECISION_TYPE_PPLX_DECIDER || type == COMMON_DECISION_TYPE_VLLM_D3 || type == COMMON_DECISION_TYPE_LFM2_D1 ||
|
||||
type == COMMON_DECISION_TYPE_H2O_LIGHTNING) {
|
||||
if (!files.empty()) {
|
||||
task.tokens = process_mtmd_prompt(mctx, prompt, files, init_opt);
|
||||
return;
|
||||
@@ -1181,7 +1501,12 @@ json server_decision_context::format_answer(const server_decision_question & que
|
||||
|
||||
// softmax over the outputs of each variant, then the average of the variants
|
||||
// lfm2-d1-omni: image and audio answers are not calibrated
|
||||
const float temperature = has_media && type == COMMON_DECISION_TYPE_LFM2_D1_OMNI ? 1.0f : get_temperature(question);
|
||||
float temperature = has_media && type == COMMON_DECISION_TYPE_LFM2_D1_OMNI ? 1.0f : get_temperature(question);
|
||||
// a model can have its own temperature for requests with media
|
||||
const auto it_image = temperatures.find("image");
|
||||
if (has_media && it_image != temperatures.end()) {
|
||||
temperature = it_image->second;
|
||||
}
|
||||
std::vector<double> probs(n, 0.0);
|
||||
for (size_t v = 0; v < scores.size(); v++) {
|
||||
const auto & s = scores[v];
|
||||
|
||||
@@ -46,6 +46,7 @@ struct server_decision_context {
|
||||
case COMMON_DECISION_TYPE_PPLX_DECIDER:
|
||||
case COMMON_DECISION_TYPE_VLLM_DECISION2:
|
||||
case COMMON_DECISION_TYPE_LFM2_D1:
|
||||
case COMMON_DECISION_TYPE_H2O_LIGHTNING:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -66,6 +67,7 @@ struct server_decision_context {
|
||||
case COMMON_DECISION_TYPE_VLLM_D3:
|
||||
case COMMON_DECISION_TYPE_LFM2_D1:
|
||||
case COMMON_DECISION_TYPE_LFM2_D1_OMNI:
|
||||
case COMMON_DECISION_TYPE_H2O_LIGHTNING:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -75,6 +77,9 @@ struct server_decision_context {
|
||||
// throw std::invalid_argument on bad input
|
||||
std::vector<server_decision_question> parse_questions(const json & body) const;
|
||||
|
||||
// the state as the model reads it, done once per request; returns the state for most models
|
||||
json prepare_state(const json & state) const;
|
||||
|
||||
// returns the state without its images, they are appended to files in order
|
||||
// images come from "files" (alias "images") and from the image_url and input_audio parts of a state made of chat messages
|
||||
// an image can be an audio clip if the model supports it
|
||||
@@ -121,7 +126,7 @@ private:
|
||||
bool strict_payload = false; // state, instructions and descriptions must be text, an object or an array
|
||||
bool canonical_legend = false; // non-string score descriptions are returned as canonical JSON text
|
||||
|
||||
// OPENJEV, LEV, NIMBLE, PPLX_DECIDER, VLLM_D3
|
||||
// OPENJEV, LEV, NIMBLE, PPLX_DECIDER, VLLM_D3, H2O_LIGHTNING
|
||||
std::vector<llama_token> labels;
|
||||
std::vector<std::string> label_texts; // only if the label of an option is given to the template
|
||||
|
||||
@@ -132,6 +137,10 @@ private:
|
||||
size_t max_head_tokens = 0; // question + options
|
||||
size_t max_option_tokens = 48;
|
||||
|
||||
// H2O_LIGHTNING
|
||||
std::vector<std::string> quoted_spellings; // texts of the special tokens, quoted in the input, longest first
|
||||
std::string quoted_first; // the first bytes of these texts
|
||||
|
||||
std::string render(
|
||||
const json & state,
|
||||
const std::vector<server_decision_question> & questions,
|
||||
@@ -143,6 +152,10 @@ private:
|
||||
size_t n_outputs(const server_decision_question & question) const;
|
||||
// LFM2_D1: label text and tokens of each option
|
||||
void d1_labels(const server_decision_question & question, std::vector<std::string> & texts, std::vector<llama_tokens> & groups) const;
|
||||
// H2O_LIGHTNING: the input of the template, its texts are rendered as the reference shim does
|
||||
json render_h2o(const json & state, const server_decision_question & question) const;
|
||||
std::string h2o_cut_state(const std::string & text) const;
|
||||
std::string h2o_quote(const std::string & text) const;
|
||||
void fill_task_laya(llama_tokens & tokens, const server_decision_question & question, server_task & task) const;
|
||||
void fill_task_d1omni(
|
||||
const json & state,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
|
||||
//
|
||||
// HTTP implementation using cpp-httplib
|
||||
@@ -744,6 +745,56 @@ void server_http_context::post(const std::string & path, const server_http_conte
|
||||
}
|
||||
}
|
||||
|
||||
void server_http_context::ws(const std::string & path, const server_http_context::ws_handler_t & handler) const {
|
||||
const std::string full_path = path_prefix + path;
|
||||
|
||||
auto callback = [handler](const httplib::Request & req, httplib::ws::WebSocket & sock) {
|
||||
struct conn_impl : server_http_context::ws_conn {
|
||||
httplib::ws::WebSocket & sock;
|
||||
std::mutex mutex_send;
|
||||
conn_impl(httplib::ws::WebSocket & sock) : sock(sock) {}
|
||||
int read(std::string & msg) override {
|
||||
switch (sock.read(msg)) {
|
||||
case httplib::ws::Text: return 1;
|
||||
case httplib::ws::Binary: return 2;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
bool send(const std::string & msg) override {
|
||||
std::lock_guard<std::mutex> lock(mutex_send);
|
||||
return sock.send(msg);
|
||||
}
|
||||
bool send_binary(const std::string & msg) override {
|
||||
std::lock_guard<std::mutex> lock(mutex_send);
|
||||
return sock.send(msg.data(), msg.size());
|
||||
}
|
||||
bool is_open() const override {
|
||||
return sock.is_open();
|
||||
}
|
||||
void close() override {
|
||||
sock.close();
|
||||
}
|
||||
};
|
||||
conn_impl conn(sock);
|
||||
const std::function<bool()> should_stop = [&sock]() {
|
||||
return !sock.is_open();
|
||||
};
|
||||
server_http_req request{
|
||||
get_params(req),
|
||||
get_headers(req),
|
||||
req.path,
|
||||
build_query_string(req),
|
||||
req.body,
|
||||
{},
|
||||
should_stop
|
||||
};
|
||||
handler(request, conn);
|
||||
};
|
||||
for (const auto & srv : pimpl->servers) {
|
||||
srv->WebSocket(full_path, callback);
|
||||
}
|
||||
}
|
||||
|
||||
void server_http_context::del(const std::string & path, const server_http_context::handler_t & handler) const {
|
||||
handlers.emplace(path, handler);
|
||||
auto callback = [handler](const httplib::Request & req, httplib::Response & res) {
|
||||
|
||||
@@ -74,6 +74,20 @@ struct server_http_context {
|
||||
using handler_t = std::function<server_http_res_ptr(const server_http_req & req)>;
|
||||
mutable std::unordered_map<std::string, handler_t> handlers;
|
||||
|
||||
// websocket connection, see ws()
|
||||
struct ws_conn {
|
||||
// blocking read, returns 1 for a text message, 2 for a binary message, 0 when the connection is closed
|
||||
virtual int read(std::string & msg) = 0;
|
||||
// thread-safe, can be called while another thread is in read()
|
||||
virtual bool send(const std::string & msg) = 0;
|
||||
virtual bool send_binary(const std::string & msg) = 0;
|
||||
virtual bool is_open() const = 0;
|
||||
virtual void close() = 0;
|
||||
virtual ~ws_conn() = default;
|
||||
};
|
||||
// the handler runs on its own HTTP thread for the whole lifetime of the connection
|
||||
using ws_handler_t = std::function<void(const server_http_req & req, ws_conn & conn)>;
|
||||
|
||||
std::string path_prefix;
|
||||
int port = 9931;
|
||||
bool is_ssl = false;
|
||||
@@ -89,6 +103,7 @@ struct server_http_context {
|
||||
void get(const std::string & path, const handler_t & handler) const;
|
||||
void post(const std::string & path, const handler_t & handler) const;
|
||||
void del(const std::string & path, const handler_t & handler) const;
|
||||
void ws(const std::string & path, const ws_handler_t & handler) const;
|
||||
|
||||
// Register the Google Cloud Platform (Vertex AI) compat (AIP_PREDICT_ROUTE env var, or /predict)
|
||||
// Must be called AFTER all other API routes are registered
|
||||
|
||||
@@ -521,27 +521,31 @@ void server_model_meta::update_caps(const common_params & base) {
|
||||
return;
|
||||
}
|
||||
|
||||
// read the output modalities and the trained context from the GGUF metadata
|
||||
std::vector<std::string> output_modalities = {"text"};
|
||||
if (!params.model.path.empty()) {
|
||||
const common_gguf_info info = common_get_gguf_info(params.model.path);
|
||||
output_modalities = server_model_output_modalities(info.decision_type);
|
||||
n_ctx_train = info.n_ctx_train;
|
||||
}
|
||||
|
||||
bool inp_image = false;
|
||||
bool inp_audio = false;
|
||||
bool gen_audio = false;
|
||||
try {
|
||||
if (!params.no_mmproj && !params.mmproj.path.empty()) {
|
||||
mtmd_caps caps = mtmd_get_cap_from_file(params.mmproj.path.c_str());
|
||||
inp_image = caps.inp_vision;
|
||||
inp_audio = caps.inp_audio;
|
||||
gen_audio = caps.gen_audio;
|
||||
}
|
||||
} catch (const std::exception & e) {
|
||||
LOG_WRN("failed to read the multimodal capabilities of '%s': %s\n", name.c_str(), e.what());
|
||||
// keep the output modalities from the GGUF metadata
|
||||
}
|
||||
|
||||
// read the output modalities and the trained context from the GGUF metadata
|
||||
std::vector<std::string> output_modalities = {"text"};
|
||||
if (!params.model.path.empty()) {
|
||||
const common_gguf_info info = common_get_gguf_info(params.model.path);
|
||||
// same rule as mtmd_helper_model_can_chat()
|
||||
const bool can_chat = !gen_audio || info.has_chat_tmpl;
|
||||
output_modalities = server_model_output_modalities(info.decision_type, can_chat, gen_audio);
|
||||
n_ctx_train = info.n_ctx_train;
|
||||
}
|
||||
|
||||
// offline discovery cannot see video; a loaded model reports it
|
||||
architecture = server_model_architecture_json(inp_image, inp_audio, false, output_modalities);
|
||||
}
|
||||
@@ -1899,6 +1903,81 @@ server_http_res_ptr server_models::proxy_request(const server_http_req & req, co
|
||||
return proxy;
|
||||
}
|
||||
|
||||
void server_models::proxy_ws(const server_http_req & req, server_http_context::ws_conn & conn, const std::string & name) {
|
||||
auto send_error = [&conn](const std::string & message) {
|
||||
conn.send(safe_json_to_str(json {
|
||||
{"type", "error"},
|
||||
{"error", {{"type", "server_error"}, {"code", nullptr}, {"message", message}, {"param", nullptr}}},
|
||||
}));
|
||||
conn.close();
|
||||
};
|
||||
|
||||
auto meta = get_meta(name);
|
||||
if (!meta.has_value() || !meta->is_running()) {
|
||||
send_error("model name=" + name + " is not running");
|
||||
return;
|
||||
}
|
||||
{
|
||||
std::unique_lock<std::mutex> lk(mutex);
|
||||
mapping[name].meta.last_used = ggml_time_ms();
|
||||
mapping[name].req_count++;
|
||||
}
|
||||
SRV_INF("proxying websocket to model %s on port %d\n", name.c_str(), meta->port);
|
||||
|
||||
std::string url = std::string("ws://") + CHILD_ADDR + ":" + std::to_string(meta->port) + req.path;
|
||||
if (!req.query_string.empty()) {
|
||||
url += '?' + req.query_string;
|
||||
}
|
||||
// the client library adds its own upgrade headers
|
||||
httplib::Headers headers;
|
||||
for (const auto & [key, value] : req.headers) {
|
||||
std::string lower = key;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
|
||||
if (lower == "host" || lower == "connection" || lower == "upgrade" || lower.compare(0, 14, "sec-websocket-") == 0) {
|
||||
continue;
|
||||
}
|
||||
headers.emplace(key, value);
|
||||
}
|
||||
|
||||
{
|
||||
httplib::ws::WebSocketClient upstream(url, headers);
|
||||
if (!upstream.connect()) {
|
||||
send_error("failed to connect to model name=" + name);
|
||||
} else {
|
||||
// client to child
|
||||
std::thread pump([&]() {
|
||||
std::string msg;
|
||||
while (true) {
|
||||
const int res = conn.read(msg);
|
||||
if (res == 0 || !(res == 1 ? upstream.send(msg) : upstream.send(msg.data(), msg.size()))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
upstream.close();
|
||||
});
|
||||
// child to client
|
||||
std::string msg;
|
||||
while (true) {
|
||||
const auto res = upstream.read(msg);
|
||||
if (res == httplib::ws::Fail || !(res == httplib::ws::Text ? conn.send(msg) : conn.send_binary(msg))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
conn.close();
|
||||
pump.join();
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> lk(mutex);
|
||||
auto it = mapping.find(name);
|
||||
if (it != mapping.end() && it->second.req_count > 0) {
|
||||
it->second.req_count--;
|
||||
if (it->second.req_count == 0) {
|
||||
sched->tick(lk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void server_models::handle_child_state(const std::string & name, const std::string & raw_input) {
|
||||
server_state state;
|
||||
json payload;
|
||||
@@ -2277,6 +2356,24 @@ void server_models_routes::init_routes() {
|
||||
return models.proxy_request(req, method, name, false);
|
||||
};
|
||||
|
||||
this->proxy_ws = [this](const server_http_req & req, server_http_context::ws_conn & conn) {
|
||||
std::string name = req.get_param("model");
|
||||
bool autoload = is_autoload(params, req);
|
||||
auto error_res = std::make_unique<server_http_res>();
|
||||
if (!router_validate_model(name, models, autoload, error_res)) {
|
||||
// error_res holds {"error": {...}}, send it as a websocket error event
|
||||
json err = json::parse(error_res->data);
|
||||
err["type"] = "error";
|
||||
conn.send(safe_json_to_str(err));
|
||||
conn.close();
|
||||
return;
|
||||
}
|
||||
if (autoload) {
|
||||
models.ensure_model_ready(name, req.should_stop);
|
||||
}
|
||||
models.proxy_ws(req, conn, name);
|
||||
};
|
||||
|
||||
this->proxy_post = [this](const server_http_req & req) {
|
||||
std::string method = "POST";
|
||||
json body = json::parse(req.body);
|
||||
@@ -2836,9 +2933,10 @@ server_http_proxy::server_http_proxy(
|
||||
cli->set_write_timeout(timeout_read, 0); // reversed for cli (client) vs srv (server)
|
||||
cli->set_read_timeout(timeout_write, 0);
|
||||
this->status = 500; // to be overwritten upon response
|
||||
this->cleanup_pipes = [pipe]() {
|
||||
this->cleanup_pipes = [pipe, cli]() {
|
||||
pipe->close_read();
|
||||
pipe->close_write();
|
||||
cli->stop();
|
||||
};
|
||||
|
||||
// wire up the receive end of the pipe
|
||||
|
||||
@@ -366,6 +366,8 @@ public:
|
||||
|
||||
// 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, bool detached = false);
|
||||
// forward a websocket connection to the child, returns when either side closes
|
||||
void proxy_ws(const server_http_req & req, server_http_context::ws_conn & conn, const std::string & name);
|
||||
|
||||
// handle message sent from server_child::notify_to_router()
|
||||
// raw input must starts with CMD_CHILD_TO_ROUTER_STATE, followed by a JSON string
|
||||
@@ -433,6 +435,8 @@ struct server_models_routes {
|
||||
server_http_context::handler_t get_router_props;
|
||||
server_http_context::handler_t proxy_get;
|
||||
server_http_context::handler_t proxy_post;
|
||||
server_http_context::ws_handler_t proxy_ws;
|
||||
|
||||
server_http_context::handler_t get_router_models;
|
||||
server_http_context::handler_t post_router_models_load;
|
||||
server_http_context::handler_t post_router_models_unload;
|
||||
|
||||
@@ -448,6 +448,7 @@ server_task_result_ptr server_response::recv(const std::unordered_set<int> & id_
|
||||
}
|
||||
|
||||
server_task_result_ptr server_response::recv_with_timeout(const std::unordered_set<int> & id_tasks, int timeout) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout);
|
||||
while (true) {
|
||||
std::unique_lock<std::mutex> lock(mutex_results);
|
||||
|
||||
@@ -459,7 +460,7 @@ server_task_result_ptr server_response::recv_with_timeout(const std::unordered_s
|
||||
}
|
||||
}
|
||||
|
||||
std::cv_status cr_res = condition_results.wait_for(lock, std::chrono::seconds(timeout));
|
||||
std::cv_status cr_res = condition_results.wait_until(lock, deadline);
|
||||
if (!running) {
|
||||
RES_DBG("%s : queue result stop\n", __func__);
|
||||
std::terminate(); // we cannot return here since the caller is HTTP code
|
||||
|
||||
@@ -511,6 +511,58 @@ task_params eval_llama_cmpl_schema(
|
||||
return params;
|
||||
}
|
||||
|
||||
//
|
||||
// TTS schema
|
||||
//
|
||||
|
||||
std::vector<std::unique_ptr<field>> make_tts_schema(server_task & task) {
|
||||
std::vector<std::unique_ptr<field>> fields;
|
||||
auto add = [&](field * f) {
|
||||
fields.emplace_back(f);
|
||||
};
|
||||
|
||||
add((new field_num("top_k", task.tts_inp.data.top_k))
|
||||
->set_limits(0, INT32_MAX)
|
||||
->set_desc("Top-k for the acoustic code predictor, 0 to use the model default"));
|
||||
|
||||
add((new field_num("top_p", task.tts_inp.data.top_p))
|
||||
->set_limits(0.0f, 1.0f)
|
||||
->set_desc("Top-p for the acoustic code predictor, 0.0 to use the model default"));
|
||||
|
||||
add((new field_num("seed", task.tts_inp.data.seed))
|
||||
->set_desc("RNG seed for the backbone sampler and the codec/vocoder (-1 = random)"));
|
||||
|
||||
add((new field_num("n_predict", task.params.n_predict))
|
||||
->set_hard_limits(-1, INT32_MAX)
|
||||
->add_alias("max_tokens")
|
||||
->set_desc("Max number of audio frames to generate"));
|
||||
|
||||
add((new field_num("repeat_penalty", task.params.sampling.penalty_repeat))
|
||||
->set_desc("Repetition penalty applied to the backbone sampler over the whole generation"));
|
||||
|
||||
add((new field_bool("stream", task.params.stream))
|
||||
->set_desc("Stream the audio as it becomes available instead of waiting for the full generation"));
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
void eval_tts_schema(const common_params & params_base, server_task & task, const json & data) {
|
||||
// baseline defaults, individual requests can override them
|
||||
// TODO @ngxson : change the default values based on model
|
||||
task.params.sampling = params_base.sampling;
|
||||
task.params.sampling.penalty_repeat = 1.05f;
|
||||
task.params.n_predict = -1;
|
||||
task.tts_inp.data.top_k = 0;
|
||||
task.tts_inp.data.top_p = 0.0f;
|
||||
task.tts_inp.data.seed = params_base.sampling.seed;
|
||||
|
||||
field_eval_context ctx(task.params);
|
||||
|
||||
for (const auto & f : make_tts_schema(task)) {
|
||||
f->eval(ctx, data);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// eval() implementations
|
||||
//
|
||||
|
||||
@@ -101,4 +101,12 @@ task_params eval_llama_cmpl_schema(
|
||||
const std::vector<llama_logit_bias> & logit_bias_eog,
|
||||
const json & data);
|
||||
|
||||
std::vector<std::unique_ptr<field>> make_tts_schema(server_task & task);
|
||||
|
||||
// evaluates the /tts request params into task.params and task.tts_inp.data
|
||||
void eval_tts_schema(
|
||||
const common_params & params_base,
|
||||
server_task & task,
|
||||
const json & data);
|
||||
|
||||
} // namespace server_schema
|
||||
|
||||
@@ -591,8 +591,10 @@ json server_task_result_cmpl_final::to_json_oaicompat_resp() {
|
||||
json server_task_result_cmpl_final::to_json_oaicompat_resp_stream() {
|
||||
std::vector<json> server_sent_events;
|
||||
std::vector<json> output;
|
||||
size_t output_index = 0;
|
||||
|
||||
if (oaicompat_msg.reasoning_content != "") {
|
||||
const size_t item_output_index = output_index++;
|
||||
const json output_item = json {
|
||||
{"id", oai_resp_reasoning_id},
|
||||
{"summary", json::array()},
|
||||
@@ -608,6 +610,7 @@ json server_task_result_cmpl_final::to_json_oaicompat_resp_stream() {
|
||||
{"event", "response.output_item.done"},
|
||||
{"data", json {
|
||||
{"type", "response.output_item.done"},
|
||||
{"output_index", item_output_index},
|
||||
{"item", output_item}
|
||||
}}
|
||||
});
|
||||
@@ -615,11 +618,14 @@ json server_task_result_cmpl_final::to_json_oaicompat_resp_stream() {
|
||||
}
|
||||
|
||||
if (oaicompat_msg.content != "") {
|
||||
const size_t item_output_index = output_index++;
|
||||
server_sent_events.push_back(json {
|
||||
{"event", "response.output_text.done"},
|
||||
{"data", json {
|
||||
{"type", "response.output_text.done"},
|
||||
{"item_id", oai_resp_message_id},
|
||||
{"output_index", item_output_index},
|
||||
{"content_index", 0},
|
||||
{"text", oaicompat_msg.content}
|
||||
}}
|
||||
});
|
||||
@@ -636,6 +642,8 @@ json server_task_result_cmpl_final::to_json_oaicompat_resp_stream() {
|
||||
{"data", json {
|
||||
{"type", "response.content_part.done"},
|
||||
{"item_id", oai_resp_message_id},
|
||||
{"output_index", item_output_index},
|
||||
{"content_index", 0},
|
||||
{"part", content_part}
|
||||
}}
|
||||
});
|
||||
@@ -651,6 +659,7 @@ json server_task_result_cmpl_final::to_json_oaicompat_resp_stream() {
|
||||
{"event", "response.output_item.done"},
|
||||
{"data", json {
|
||||
{"type", "response.output_item.done"},
|
||||
{"output_index", item_output_index},
|
||||
{"item", output_item}
|
||||
}}
|
||||
});
|
||||
@@ -658,6 +667,7 @@ json server_task_result_cmpl_final::to_json_oaicompat_resp_stream() {
|
||||
}
|
||||
|
||||
for (const common_chat_tool_call & tool_call : oaicompat_msg.tool_calls) {
|
||||
const size_t item_output_index = output_index++;
|
||||
const json output_item = {
|
||||
{"id", "fc_" + tool_call.id},
|
||||
{"type", "function_call"},
|
||||
@@ -670,6 +680,7 @@ json server_task_result_cmpl_final::to_json_oaicompat_resp_stream() {
|
||||
{"event", "response.output_item.done"},
|
||||
{"data", json {
|
||||
{"type", "response.output_item.done"},
|
||||
{"output_index", item_output_index},
|
||||
{"item", output_item}
|
||||
}}
|
||||
});
|
||||
@@ -1202,6 +1213,7 @@ json server_task_result_cmpl_partial::to_json_oaicompat_resp() {
|
||||
{"event", "response.output_item.added"},
|
||||
{"data", json {
|
||||
{"type", "response.output_item.added"},
|
||||
{"output_index", 0},
|
||||
{"item", json {
|
||||
{"id", oai_resp_reasoning_id},
|
||||
{"summary", json::array()},
|
||||
@@ -1218,6 +1230,8 @@ json server_task_result_cmpl_partial::to_json_oaicompat_resp() {
|
||||
{"event", "response.reasoning_text.delta"},
|
||||
{"data", json {
|
||||
{"type", "response.reasoning_text.delta"},
|
||||
{"output_index", 0},
|
||||
{"content_index", 0},
|
||||
{"delta", diff.reasoning_content_delta},
|
||||
{"item_id", oai_resp_reasoning_id},
|
||||
}},
|
||||
@@ -1230,6 +1244,7 @@ json server_task_result_cmpl_partial::to_json_oaicompat_resp() {
|
||||
{"event", "response.output_item.added"},
|
||||
{"data", json {
|
||||
{"type", "response.output_item.added"},
|
||||
{"output_index", anthropic_has_reasoning ? 1 : 0},
|
||||
{"item", json {
|
||||
{"content", json::array()},
|
||||
{"id", oai_resp_message_id},
|
||||
@@ -1244,6 +1259,8 @@ json server_task_result_cmpl_partial::to_json_oaicompat_resp() {
|
||||
{"data", json {
|
||||
{"type", "response.content_part.added"},
|
||||
{"item_id", oai_resp_message_id},
|
||||
{"output_index", anthropic_has_reasoning ? 1 : 0},
|
||||
{"content_index", 0},
|
||||
{"part", json {
|
||||
{"type", "output_text"},
|
||||
{"text", ""},
|
||||
@@ -1257,6 +1274,8 @@ json server_task_result_cmpl_partial::to_json_oaicompat_resp() {
|
||||
{"data", json {
|
||||
{"type", "response.output_text.delta"},
|
||||
{"item_id", oai_resp_message_id},
|
||||
{"output_index", anthropic_has_reasoning ? 1 : 0},
|
||||
{"content_index", 0},
|
||||
{"delta", diff.content_delta},
|
||||
}},
|
||||
});
|
||||
@@ -1267,6 +1286,7 @@ json server_task_result_cmpl_partial::to_json_oaicompat_resp() {
|
||||
{"event", "response.output_item.added"},
|
||||
{"data", json {
|
||||
{"type", "response.output_item.added"},
|
||||
{"output_index", (anthropic_has_reasoning ? 1 : 0) + (text_block_started ? 1 : 0) + diff.tool_call_index},
|
||||
{"item", json {
|
||||
{"id", "fc_" + diff.tool_call_delta.id},
|
||||
{"arguments", ""},
|
||||
@@ -1287,6 +1307,7 @@ json server_task_result_cmpl_partial::to_json_oaicompat_resp() {
|
||||
{"type", "response.function_call_arguments.delta"},
|
||||
{"delta", diff.tool_call_delta.arguments},
|
||||
{"item_id", "fc_" + oai_resp_fc_id},
|
||||
{"output_index", (anthropic_has_reasoning ? 1 : 0) + (text_block_started ? 1 : 0) + diff.tool_call_index},
|
||||
}},
|
||||
});
|
||||
}
|
||||
@@ -1487,6 +1508,17 @@ json server_task_result_rerank::to_json() {
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// server_task_result_tts
|
||||
//
|
||||
json server_task_result_tts::to_json() {
|
||||
return json {
|
||||
{"sample_rate", sample_rate},
|
||||
{"n_bytes", audio.size()},
|
||||
{"final", final},
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// server_task_result_decision
|
||||
//
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
// TODO: prevent including the whole server-common.h as we only use server_tokens
|
||||
#include "server-common.h"
|
||||
#include "mtmd-helper.h"
|
||||
|
||||
|
||||
enum server_task_type {
|
||||
@@ -18,6 +19,7 @@ enum server_task_type {
|
||||
SERVER_TASK_TYPE_RERANK,
|
||||
SERVER_TASK_TYPE_DECISION,
|
||||
SERVER_TASK_TYPE_INFILL,
|
||||
SERVER_TASK_TYPE_TRANSCRIBE_RT,
|
||||
SERVER_TASK_TYPE_CANCEL,
|
||||
SERVER_TASK_TYPE_CONTROL,
|
||||
SERVER_TASK_TYPE_NEXT_RESPONSE,
|
||||
@@ -28,6 +30,7 @@ enum server_task_type {
|
||||
SERVER_TASK_TYPE_SLOT_ERASE,
|
||||
SERVER_TASK_TYPE_GET_LORA,
|
||||
SERVER_TASK_TYPE_SET_LORA,
|
||||
SERVER_TASK_TYPE_TTS,
|
||||
};
|
||||
|
||||
// TODO: change this to more generic "response_format" to replace the "format_response_*" in server-common
|
||||
@@ -48,6 +51,14 @@ enum stop_type {
|
||||
STOP_TYPE_LIMIT,
|
||||
};
|
||||
|
||||
enum server_control_action {
|
||||
SERVER_CONTROL_ACTION_NONE,
|
||||
SERVER_CONTROL_ACTION_REASONING_END,
|
||||
SERVER_CONTROL_ACTION_AUDIO_APPEND, // push control_audio into the slot's audio stream
|
||||
SERVER_CONTROL_ACTION_AUDIO_COMMIT, // flush the audio stream, more audio can follow
|
||||
SERVER_CONTROL_ACTION_AUDIO_END, // flush the audio stream and end it
|
||||
};
|
||||
|
||||
struct task_params {
|
||||
bool stream = false;
|
||||
bool include_usage = false;
|
||||
@@ -86,8 +97,9 @@ struct task_params {
|
||||
std::string oaicompat_cmpl_id;
|
||||
|
||||
// realtime control (SERVER_TASK_TYPE_CONTROL)
|
||||
std::string control_action;
|
||||
std::string control_cmpl_id;
|
||||
server_control_action control_action = SERVER_CONTROL_ACTION_NONE;
|
||||
std::string control_cmpl_id;
|
||||
std::vector<float> control_audio; // PCM F32 samples for SERVER_CONTROL_ACTION_AUDIO_APPEND
|
||||
|
||||
// reported in generation_settings, parsing itself is owned by the chat session
|
||||
common_chat_format chat_format = COMMON_CHAT_FORMAT_CONTENT_ONLY;
|
||||
@@ -209,6 +221,9 @@ struct server_task {
|
||||
// used by SERVER_TASK_TYPE_SET_LORA
|
||||
std::map<int, float> set_lora; // mapping adapter ID -> scale
|
||||
|
||||
// used by SERVER_TASK_TYPE_TTS
|
||||
mtmd_helper::gen_audio::inp tts_inp;
|
||||
|
||||
server_task() = default;
|
||||
|
||||
server_task(server_task_type type) : type(type) {}
|
||||
@@ -233,6 +248,7 @@ struct server_task {
|
||||
switch (type) {
|
||||
case SERVER_TASK_TYPE_COMPLETION:
|
||||
case SERVER_TASK_TYPE_INFILL:
|
||||
case SERVER_TASK_TYPE_TRANSCRIBE_RT:
|
||||
return true;
|
||||
case SERVER_TASK_TYPE_DECISION:
|
||||
return !decision.labels.empty();
|
||||
@@ -245,6 +261,8 @@ struct server_task {
|
||||
switch (type) {
|
||||
case SERVER_TASK_TYPE_COMPLETION:
|
||||
case SERVER_TASK_TYPE_INFILL:
|
||||
case SERVER_TASK_TYPE_TTS:
|
||||
case SERVER_TASK_TYPE_TRANSCRIBE_RT:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -514,6 +532,16 @@ struct server_task_result_embd : server_task_result {
|
||||
json to_json_oaicompat();
|
||||
};
|
||||
|
||||
struct server_task_result_tts : server_task_result {
|
||||
std::string audio; // raw bytes for this chunk (WAV or PCM, per request's out_type)
|
||||
int32_t sample_rate = 0;
|
||||
bool final = false; // true for the last chunk of a request
|
||||
|
||||
virtual bool is_stop() override { return final; }
|
||||
|
||||
virtual json to_json() override;
|
||||
};
|
||||
|
||||
struct server_task_result_rerank : server_task_result {
|
||||
float score = -1e6;
|
||||
|
||||
|
||||
@@ -247,6 +247,8 @@ int llama_server(common_params & params, int argc, char ** argv, server_child &
|
||||
routes.post_control = models_routes->proxy_post;
|
||||
routes.post_responses_oai = models_routes->proxy_post;
|
||||
routes.post_transcriptions_oai = models_routes->proxy_post;
|
||||
routes.post_tts = models_routes->proxy_post;
|
||||
routes.ws_realtime = models_routes->proxy_ws;
|
||||
routes.post_anthropic_messages = models_routes->proxy_post;
|
||||
routes.post_anthropic_count_tokens = models_routes->proxy_post;
|
||||
routes.post_infill = models_routes->proxy_post;
|
||||
@@ -292,6 +294,8 @@ int llama_server(common_params & params, int argc, char ** argv, server_child &
|
||||
ctx_http.post("/responses", ex_wrapper(routes.post_responses_oai));
|
||||
ctx_http.post("/v1/audio/transcriptions", ex_wrapper(routes.post_transcriptions_oai));
|
||||
ctx_http.post("/audio/transcriptions", ex_wrapper(routes.post_transcriptions_oai));
|
||||
ctx_http.post("/tts", ex_wrapper(routes.post_tts));
|
||||
ctx_http.ws ("/v1/realtime", routes.ws_realtime);
|
||||
ctx_http.post("/v1/messages", ex_wrapper(routes.post_anthropic_messages)); // anthropic messages API
|
||||
ctx_http.post("/infill", ex_wrapper(routes.post_infill));
|
||||
ctx_http.post("/embedding", ex_wrapper(routes.post_embeddings)); // legacy
|
||||
|
||||
@@ -587,6 +587,10 @@ def test_chat_completions_multiple_choices():
|
||||
for choice in res.body["choices"]:
|
||||
assert "assistant" == choice["message"]["role"]
|
||||
assert choice["finish_reason"] == "length"
|
||||
# each choice generates max_tokens tokens, usage counts them all
|
||||
# ref: https://github.com/ggml-org/llama.cpp/issues/29451
|
||||
assert res.body["usage"]["completion_tokens"] == 16
|
||||
assert res.body["usage"]["total_tokens"] == res.body["usage"]["prompt_tokens"] + 16
|
||||
|
||||
|
||||
def test_chat_completions_token_count():
|
||||
|
||||
@@ -229,3 +229,39 @@ def test_multi_requests_parallel(n_slots: int, n_requests: int):
|
||||
for res in results:
|
||||
assert res.status_code == 200
|
||||
assert match_regex("(wise|kind|owl|answer)+", res.body["content"])
|
||||
|
||||
|
||||
def test_draft_token_probs():
|
||||
# tokens accepted from the draft must carry the target model's probabilities,
|
||||
# same as without speculative decoding (#24271)
|
||||
global server
|
||||
request = {
|
||||
"prompt": "I believe the meaning of life is",
|
||||
"temperature": 0.0,
|
||||
"top_k": 1,
|
||||
"n_predict": 16,
|
||||
"n_probs": 4,
|
||||
}
|
||||
|
||||
server.model_draft = None # disable draft model
|
||||
server.spec_type = None
|
||||
server.start()
|
||||
res = server.make_request("POST", "/completion", data=request)
|
||||
assert res.status_code == 200
|
||||
probs_no_draft = res.body["completion_probabilities"]
|
||||
server.stop()
|
||||
|
||||
create_server()
|
||||
server.start()
|
||||
res = server.make_request("POST", "/completion", data=request)
|
||||
assert res.status_code == 200
|
||||
assert res.body["timings"]["draft_n"] > 0
|
||||
probs_draft = res.body["completion_probabilities"]
|
||||
|
||||
assert len(probs_draft) == len(probs_no_draft)
|
||||
for tok_draft, tok_no_draft in zip(probs_draft, probs_no_draft):
|
||||
assert tok_draft["id"] == tok_no_draft["id"]
|
||||
assert len(tok_draft["top_logprobs"]) == len(tok_no_draft["top_logprobs"])
|
||||
assert abs(tok_draft["logprob"] - tok_no_draft["logprob"]) < 0.25
|
||||
# real logprobs are not all exactly 0.0 (prob 1.0)
|
||||
assert any(tok["logprob"] < -1e-4 for tok in probs_draft)
|
||||
|
||||
@@ -12,6 +12,8 @@ Simple usage:
|
||||
llama-tts -hf ggml-org/Qwen3-TTS-12Hz-1.7B-Base-GGUF -p "Hello world" --output out.wav
|
||||
```
|
||||
|
||||
Note: it's highly recommended to always provide a speaker reference voice (via `--tts-speaker-file`); otherwise, the model's performance may be degraded.
|
||||
|
||||
Common params:
|
||||
- Sampling params such as `--top-k`, `--top-p`, `--temp`, etc.
|
||||
- `-n <number_of_frames>` limits the output length, e.g. `-n 500`. Note that how many milliseconds each frame represents varies by model
|
||||
|
||||
+3
-3
@@ -111,8 +111,9 @@ int main(int argc, char ** argv) {
|
||||
speaker_bitmap.reset(wrapper.bitmap);
|
||||
}
|
||||
|
||||
mtmd_helper::gen_audio gen(lctx, mctx.get());
|
||||
mtmd_helper_gen_audio_inp inp{};
|
||||
mtmd_helper::gen_audio gen;
|
||||
gen.init(lctx, mctx.get());
|
||||
mtmd_helper_gen_audio_inp inp = mtmd_helper_gen_audio_inp_default();
|
||||
inp.seq_id = 0;
|
||||
inp.prompt = params.prompt.c_str();
|
||||
inp.prompt_len = params.prompt.size();
|
||||
@@ -121,7 +122,6 @@ int main(int argc, char ** argv) {
|
||||
inp.top_k = params.sampling.top_k;
|
||||
inp.top_p = params.sampling.top_p;
|
||||
inp.seed = params.sampling.seed;
|
||||
inp.out_type = MTMD_HELPER_GEN_AUDIO_OUTTYPE_WAV;
|
||||
|
||||
//
|
||||
// stage 1: process prompt via backbone model, generate semantic representation
|
||||
|
||||
Reference in New Issue
Block a user