Compare commits

..
7 changed files with 104 additions and 18 deletions
+4 -3
View File
@@ -84,17 +84,18 @@ The floating-point output is reconstructed as
Y[r, o] ~= A[r, o] * s_x[r] * s_w[o] + b[o]
```
The packed runtime activation tensor contains the I8 activation rows and their floating-point row scales. Linear layers that share the same input and convrot group size reuse this packed tensor, avoiding repeated rotation and activation quantization within the graph.
The packed runtime activation tensor contains the I8 activation rows and their floating-point row scales. Linear layers with an input scale of `1` that share the same input and convrot group size reuse this packed tensor, avoiding repeated rotation and activation quantization within the graph. For other input scales, activations are scaled before explicit convrot quantization, and the linear output is unscaled before adding bias.
## Backend support
- CPU provides the portable regular Hadamard, activation quantization, INT8 matrix multiplication, and scale restoration implementations.
- NVIDIA CUDA devices with compute capability 7.5 or newer use the native accelerated path. For H256, CUDA fuses the rotation, row-wise maximum reduction, and activation quantization. It uses cuBLAS for I8 x I8 to I32 GEMM and a CUDA kernel for scale restoration and bias addition.
- Vulkan and other GPU backends do not currently have dedicated INT8 convrot kernels. They use the backend scheduler to fall back to CPU, which is expected to be substantially slower than the CUDA path.
- AMD HIP devices in the CDNA, RDNA3 (including RDNA3.5), and RDNA4 families use the same INT8/H256 kernels with hipBLAS for I8 x I8 to I32 GEMM. Other AMD architectures fall back to CPU for INT8 matrix multiplication.
- Vulkan provides native H256 activation quantization and INT8 matrix multiplication when the build and device support accelerated packed INT8 dot products. Unsupported configurations and GPU backends without these kernels use the backend scheduler to fall back to CPU.
LoRA adapters are applied at runtime without modifying the INT8 weights. The INT8 convrot path computes the base linear output, while LoRA, LoHa, LoKr, and raw weight-difference adapters compute their output corrections from the original, unrotated activation and add them to the base output. `--lora-apply-mode auto` selects this path for models containing INT8 tensorwise weights. If `immediately` is requested, sd.cpp falls back to runtime application because merging an adapter would require dequantizing and rotating its weight update, then recalculating the per-row scales and requantizing the result.
The dedicated CUDA convrot activation path currently requires a group size of `256`; other supported group sizes use CPU execution.
The dedicated CUDA and HIP convrot activation paths currently require a group size of `256`; other supported group sizes use CPU execution.
## Example
+1 -1
Submodule ggml updated: 4e86b56f16...89c4413f5d
+8 -2
View File
@@ -255,14 +255,20 @@ ggml_tensor* ggml_ext_linear_i8_tensorwise(ggml_context* ctx,
}
ggml_tensor* fused_bias = scale == 1.f ? b : nullptr;
auto mul_mat = [&](ggml_tensor* input) {
if (input->type == GGML_TYPE_F32 && convrot_group_size > 0) {
input = ggml_quantize_i8_convrot(ctx, input, convrot_group_size);
}
return ggml_mul_mat_i8_tensorwise(ctx, w, input, weight_scale, fused_bias, convrot_group_size);
};
if (x->ne[2] * x->ne[3] > 1024) {
int64_t ne2 = x->ne[2];
int64_t ne3 = x->ne[3];
x = ggml_reshape_2d(ctx, x, x->ne[0], x->ne[1] * x->ne[2] * x->ne[3]);
x = ggml_mul_mat_i8_tensorwise(ctx, w, x, weight_scale, fused_bias, convrot_group_size);
x = mul_mat(x);
x = ggml_reshape_4d(ctx, x, x->ne[0], x->ne[1] / ne2 / ne3, ne2, ne3);
} else {
x = ggml_mul_mat_i8_tensorwise(ctx, w, x, weight_scale, fused_bias, convrot_group_size);
x = mul_mat(x);
}
if (scale != 1.f) {
+74 -5
View File
@@ -1,6 +1,7 @@
#ifndef __SD_MODEL_COMMON_GGML_BLOCK_HPP__
#define __SD_MODEL_COMMON_GGML_BLOCK_HPP__
#include <cmath>
#include <cstdint>
#include <map>
#include <memory>
@@ -335,15 +336,75 @@ class Embedding : public UnaryBlock {
protected:
int64_t embedding_dim;
int64_t num_embeddings;
bool is_int8_tensorwise = false;
int int8_convrot_group_size = 0;
static void get_rows_i8(ggml_tensor* dst, int ith, int nth, void* userdata) {
const auto* embedding = static_cast<const Embedding*>(userdata);
const int group_size = embedding->int8_convrot_group_size;
const auto* weight = dst->src[0];
const auto* input_ids = static_cast<const int32_t*>(dst->src[1]->data);
const auto* scales = static_cast<const float*>(dst->src[2]->data);
const bool scalar_scale = ggml_nelements(dst->src[2]) == 1;
const float normalization = group_size > 0 ? 1.f / std::sqrt(static_cast<float>(group_size)) : 1.f;
for (int64_t row = ith; row < dst->ne[1]; row += nth) {
const int32_t token = input_ids[row];
GGML_ASSERT(token >= 0 && token < weight->ne[1]);
const auto* src = reinterpret_cast<const int8_t*>(static_cast<const char*>(weight->data) + token * weight->nb[1]);
auto* out = reinterpret_cast<float*>(static_cast<char*>(dst->data) + row * dst->nb[1]);
const float scale = scales[scalar_scale ? 0 : token] * normalization;
for (int64_t i = 0; i < weight->ne[0]; ++i) {
out[i] = static_cast<float>(src[i]) * scale;
}
// The regular Hadamard rotation is symmetric and its own inverse.
for (int stride = 1; stride < group_size; stride *= 4) {
for (int64_t base = 0; base < weight->ne[0]; base += 4 * stride) {
for (int j = 0; j < stride; ++j) {
float* values = out + base + j;
const float a = values[0];
const float b = values[stride];
const float c = values[2 * stride];
const float d = values[3 * stride];
values[0] = a + b + c - d;
values[stride] = a + b - c + d;
values[2 * stride] = a - b + c + d;
values[3 * stride] = -a + b + c + d;
}
}
}
}
}
void init_params(ggml_context* ctx, const String2TensorStorage& tensor_storage_map, const std::string prefix = "") override {
enum ggml_type wtype = get_type(prefix + "weight", tensor_storage_map, GGML_TYPE_F32);
if (!support_get_rows(wtype)) {
auto weight_storage = tensor_storage_map.find(prefix + "weight");
is_int8_tensorwise = weight_storage != tensor_storage_map.end() && weight_storage->second.is_int8_tensorwise;
int8_convrot_group_size = 0;
enum ggml_type wtype = get_type(prefix + "weight", tensor_storage_map, GGML_TYPE_F32);
if (is_int8_tensorwise) {
GGML_ASSERT(wtype == GGML_TYPE_I8);
auto scale_storage = tensor_storage_map.find(prefix + "weight_scale");
GGML_ASSERT(scale_storage != tensor_storage_map.end());
const int64_t scale_nelements = scale_storage->second.nelements();
GGML_ASSERT(scale_nelements == 1 || scale_nelements == num_embeddings);
params["weight_scale"] = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, scale_nelements);
if (weight_storage->second.int8_convrot) {
int8_convrot_group_size = weight_storage->second.int8_convrot_group_size;
int remainder = int8_convrot_group_size;
while (remainder > 1 && remainder % 4 == 0) {
remainder /= 4;
}
GGML_ASSERT(remainder == 1 && embedding_dim % int8_convrot_group_size == 0);
}
} else if (!support_get_rows(wtype)) {
wtype = GGML_TYPE_F32;
}
params["weight"] = ggml_new_tensor_2d(ctx, wtype, embedding_dim, num_embeddings);
}
enum ggml_op param_usage_op(const std::string& name) const override {
if (is_int8_tensorwise) {
return GGML_OP_CUSTOM;
}
return name == "weight" ? GGML_OP_GET_ROWS : GGML_OP_NONE;
}
@@ -363,9 +424,17 @@ public:
int64_t n = input_ids->ne[1];
input_ids = ggml_reshape_1d(ctx->ggml_ctx, input_ids, input_ids->ne[0] * input_ids->ne[1]);
input_ids = ggml_reshape_3d(ctx->ggml_ctx, input_ids, input_ids->ne[0], 1, input_ids->ne[1]);
auto embedding = ggml_get_rows(ctx->ggml_ctx, weight, input_ids);
embedding = ggml_reshape_3d(ctx->ggml_ctx, embedding, embedding->ne[0], embedding->ne[1] / n, n);
ggml_tensor* embedding;
if (is_int8_tensorwise) {
GGML_ASSERT(input_ids->type == GGML_TYPE_I32);
ggml_tensor* args[] = {weight, input_ids, params["weight_scale"]};
embedding = ggml_custom_4d(ctx->ggml_ctx, GGML_TYPE_F32, embedding_dim, input_ids->ne[0], 1, 1,
args, 3, get_rows_i8, GGML_N_TASKS_MAX, this);
} else {
input_ids = ggml_reshape_3d(ctx->ggml_ctx, input_ids, input_ids->ne[0], 1, input_ids->ne[1]);
embedding = ggml_get_rows(ctx->ggml_ctx, weight, input_ids);
}
embedding = ggml_reshape_3d(ctx->ggml_ctx, embedding, embedding->ne[0], embedding->ne[1] / n, n);
// [N, n_token, embedding_dim]
return embedding;
+5 -5
View File
@@ -299,22 +299,22 @@ namespace LLM {
if (contains(name, "attn.q_proj")) {
config.llama_cpp_style = true;
}
if (contains(name, "visual.patch_embed.proj.1.weight")) {
if (ends_with(name, "visual.patch_embed.proj.1.weight")) {
config.vision.split_patch_embed = true;
}
if (contains(name, "visual.patch_embed.proj.0.weight")) {
if (ends_with(name, "visual.patch_embed.proj.0.weight")) {
config.vision.patch_size = static_cast<int>(tensor_storage.ne[0]);
config.vision.in_channels = tensor_storage.ne[2];
config.vision.hidden_size = tensor_storage.ne[3];
}
// HF-format checkpoints keep the patch embed unsplit under a single name.
if (contains(name, "visual.patch_embed.proj.weight")) {
if (ends_with(name, "visual.patch_embed.proj.weight")) {
config.vision.patch_size = static_cast<int>(tensor_storage.ne[0]);
}
if (contains(name, "visual.patch_embed.bias") || contains(name, "visual.patch_embed.proj.bias")) {
config.vision.hidden_size = tensor_storage.ne[0];
}
if (contains(name, "visual.pos_embed.weight")) {
if (ends_with(name, "visual.pos_embed.weight") && tensor_storage.n_dims == 2) {
config.vision.hidden_size = tensor_storage.ne[0];
config.vision.num_position_embeddings = static_cast<int>(tensor_storage.ne[1]);
}
@@ -348,7 +348,7 @@ namespace LLM {
}
}
}
if (contains(name, "embed_tokens.weight")) {
if (ends_with(name, "embed_tokens.weight") && tensor_storage.n_dims == 2) {
config.hidden_size = tensor_storage.ne[0];
config.vocab_size = tensor_storage.ne[1];
}
+10 -1
View File
@@ -79,6 +79,10 @@ static bool device_supports_param_op(ggml_backend_dev_t device,
if (op == GGML_OP_GET_ROWS) {
ggml_tensor* indices = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, 1);
op_tensor = ggml_get_rows(ctx, weight, indices);
} else if (op == GGML_OP_CUSTOM) {
op_tensor = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 1);
op_tensor->op = op;
op_tensor->src[0] = weight;
}
if (op_tensor == nullptr) {
ggml_free(ctx);
@@ -507,7 +511,9 @@ bool ModelManager::stage_tensors_to_compute_backend(const std::vector<TensorStat
LOG_ERROR("model manager params backend is null for tensor '%s'", state->name.c_str());
return false;
}
if (state->compute_backend == state->params_backend || state->staged_to_compute_backend) {
// Custom CPU operators must retain host weights even when the runner uses a GPU.
if (state->usage_op == GGML_OP_CUSTOM ||
state->compute_backend == state->params_backend || state->staged_to_compute_backend) {
continue;
}
if (!state->loaded_to_params_backend || state->tensor == nullptr || state->tensor->data == nullptr) {
@@ -1402,6 +1408,9 @@ size_t ModelManager::compute_backend_alloc_size(const std::vector<TensorState*>&
if (state == nullptr || state->tensor == nullptr) {
continue;
}
if (state->usage_op == GGML_OP_CUSTOM && !sd_backend_is_cpu(state->compute_backend)) {
continue;
}
const bool compute_resident =
state->compute_backend == state->params_backend
? state->loaded_to_params_backend
+2 -1
View File
@@ -210,7 +210,7 @@ WeightPrefetchResult ModelManager::prefetch_params(
is_optional_missing_tensor(state->name)) {
continue;
}
if (state->compute_backend == state->params_backend) {
if (state->usage_op == GGML_OP_CUSTOM || state->compute_backend == state->params_backend) {
needs_synchronous_load = needs_synchronous_load ||
!state->loaded_to_params_backend;
continue;
@@ -275,6 +275,7 @@ bool ModelManager::activate_prefetched_params(
[&](TensorState* state) {
return state == nullptr || should_ignore(*state) ||
is_optional_missing_tensor(state->name) ||
state->usage_op == GGML_OP_CUSTOM ||
state->compute_backend == state->params_backend ||
state->staged_to_compute_backend;
});