mirror of
https://github.com/gufo-org/gufo.git
synced 2026-11-07 09:35:59 -10:00
785 lines
33 KiB
C++
785 lines
33 KiB
C++
#include "src/models/embedding_gemma2/hip/encoder_runtime.hpp"
|
|
|
|
#include <hip/hip_runtime.h>
|
|
#include <hipblas/hipblas.h>
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cmath>
|
|
#include <condition_variable>
|
|
#include <cstring>
|
|
#include <exception>
|
|
#include <map>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <utility>
|
|
|
|
#include "src/core/hip/hip_utils.hpp"
|
|
#include "src/models/embedding_gemma2/hip/blas.hpp"
|
|
#include "src/models/embedding_gemma2/hip/ops.hpp"
|
|
#include "src/models/embedding_gemma2/hip/vision_ops.hpp"
|
|
#include "src/models/embedding_gemma2/loader.hpp"
|
|
|
|
namespace gufo::models::embedding_gemma2::hip {
|
|
namespace {
|
|
|
|
/// Owning device allocation.
|
|
class DeviceBuffer {
|
|
public:
|
|
DeviceBuffer() = default;
|
|
explicit DeviceBuffer(std::size_t bytes) : bytes_(bytes) {
|
|
HIP_CHECK(hipMalloc(&data_, bytes));
|
|
}
|
|
~DeviceBuffer() {
|
|
if (data_ != nullptr) {
|
|
gufo::hip::LogCleanupError(hipFree(data_));
|
|
}
|
|
}
|
|
DeviceBuffer(const DeviceBuffer&) = delete;
|
|
DeviceBuffer& operator=(const DeviceBuffer&) = delete;
|
|
DeviceBuffer(DeviceBuffer&& other) noexcept
|
|
: data_(std::exchange(other.data_, nullptr)),
|
|
bytes_(std::exchange(other.bytes_, 0)) {}
|
|
DeviceBuffer& operator=(DeviceBuffer&& other) noexcept {
|
|
if (this != &other) {
|
|
if (data_ != nullptr) {
|
|
gufo::hip::LogCleanupError(hipFree(data_));
|
|
}
|
|
data_ = std::exchange(other.data_, nullptr);
|
|
bytes_ = std::exchange(other.bytes_, 0);
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
template<typename T = void>
|
|
[[nodiscard]] T* get() const {
|
|
return static_cast<T*>(data_);
|
|
}
|
|
[[nodiscard]] std::size_t bytes() const { return bytes_; }
|
|
|
|
void Upload(std::size_t offset, const void* host, std::size_t bytes) const {
|
|
HIP_CHECK(hipMemcpy(static_cast<std::byte*>(data_) + offset, host, bytes,
|
|
hipMemcpyHostToDevice));
|
|
}
|
|
|
|
/// Queues the copy on `stream`, ordered with the kernels that read it. The
|
|
/// host memory must stay valid until the stream is synchronized.
|
|
void UploadAsync(std::size_t offset, const void* host, std::size_t bytes,
|
|
hipStream_t stream) const {
|
|
HIP_CHECK(hipMemcpyAsync(static_cast<std::byte*>(data_) + offset, host,
|
|
bytes, hipMemcpyHostToDevice, stream));
|
|
}
|
|
|
|
private:
|
|
void* data_{nullptr};
|
|
std::size_t bytes_{0};
|
|
};
|
|
|
|
/// Concatenates BF16 tensors row-wise into one device buffer. The tensors
|
|
/// must share a column count.
|
|
DeviceBuffer UploadConcatenated(const TensorStore& store,
|
|
const std::vector<std::string>& names) {
|
|
std::size_t total = 0;
|
|
for (const std::string& name : names) {
|
|
total += store.Find(name)->byte_count;
|
|
}
|
|
DeviceBuffer buffer(total);
|
|
std::size_t offset = 0;
|
|
for (const std::string& name : names) {
|
|
const Tensor* tensor = store.Find(name);
|
|
buffer.Upload(offset, tensor->data, tensor->byte_count);
|
|
offset += tensor->byte_count;
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
/// Rows per chunk when a GEMM and its consumer run back to back. Chunking
|
|
/// bounds the float32 scratch. Unchunked, the gate/up buffer for 8,192 tokens
|
|
/// would be 8,192 x 4,096 x 4 bytes = 134 MB and the QKV buffer about 100 MB.
|
|
constexpr std::size_t kChunkTokens = 1024;
|
|
// Gemm pads a chunk up to a row bucket, so the scratch behind every chunk must
|
|
// hold a full chunk.
|
|
static_assert(kMaxSequenceTokens % kChunkTokens == 0);
|
|
|
|
/// Rotary cos/sin for every position up to the context limit, one pair of
|
|
/// [position][head_dim / 2] tables per head width and base. The code computes
|
|
/// them in double precision so the float tables are correctly rounded.
|
|
struct RopeTable {
|
|
DeviceBuffer cos;
|
|
DeviceBuffer sin;
|
|
};
|
|
|
|
RopeTable BuildRopeTable(std::uint32_t head_dim, double theta,
|
|
std::size_t positions) {
|
|
const std::size_t half = head_dim / 2U;
|
|
std::vector<float> cosines(positions * half);
|
|
std::vector<float> sines(positions * half);
|
|
for (std::size_t i = 0; i < half; ++i) {
|
|
const double frequency =
|
|
std::pow(theta, -static_cast<double>(2U * i) / head_dim);
|
|
for (std::size_t p = 0; p < positions; ++p) {
|
|
const double angle = static_cast<double>(p) * frequency;
|
|
cosines[p * half + i] = static_cast<float>(std::cos(angle));
|
|
sines[p * half + i] = static_cast<float>(std::sin(angle));
|
|
}
|
|
}
|
|
RopeTable table{DeviceBuffer(cosines.size() * sizeof(float)),
|
|
DeviceBuffer(sines.size() * sizeof(float))};
|
|
table.cos.Upload(0, cosines.data(), cosines.size() * sizeof(float));
|
|
table.sin.Upload(0, sines.data(), sines.size() * sizeof(float));
|
|
return table;
|
|
}
|
|
|
|
struct LayerWeights {
|
|
DeviceBuffer input_norm, post_attention_norm, pre_feedforward_norm,
|
|
post_feedforward_norm, layer_scalar;
|
|
DeviceBuffer qkv, output, query_norm, key_norm;
|
|
DeviceBuffer gate_up, down;
|
|
DeviceBuffer ple_gate, ple_projection, ple_post_norm;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
struct EncoderRuntime::Impl {
|
|
LoadResult loaded;
|
|
std::string model_dir;
|
|
struct Vision {
|
|
DeviceBuffer input, position_table, projection;
|
|
std::vector<LayerWeights> layers;
|
|
DeviceBuffer pixels, pixels_bf16, state, normed, qkv, query, key, value,
|
|
context, temp, gate_up, act, pooled, features;
|
|
};
|
|
std::unique_ptr<Vision> vision;
|
|
hipStream_t stream{nullptr};
|
|
hipblasHandle_t blas{nullptr};
|
|
GemmLt gemm_lt;
|
|
|
|
// Host staging for the per-batch index uploads. Asynchronous copies read
|
|
// these until the stream is synchronized, so they outlive RunBatch.
|
|
std::vector<std::uint32_t> host_ids, host_positions, host_begin, host_end,
|
|
host_offsets;
|
|
|
|
DeviceBuffer embed_table, final_norm, embedding_projection, ple_projection,
|
|
ple_norm;
|
|
std::vector<LayerWeights> layers;
|
|
std::map<std::pair<std::uint32_t, float>, RopeTable> rope;
|
|
|
|
// Scratch, sized for kMaxSequenceTokens.
|
|
DeviceBuffer token_ids, positions, sequence_begin, sequence_end, offsets;
|
|
DeviceBuffer state, embeddings_bf16, normed_bf16, qkv_f32, query_bf16,
|
|
key_bf16, value_bf16, value_t_bf16, context_bf16, temp_f32, gate_up_f32,
|
|
act_bf16, ple_f32, gate_f32, projected_f32, pooled_f32;
|
|
// Target of the device probe that runs after a failed batch.
|
|
DeviceBuffer probe;
|
|
std::atomic<bool> lost{false};
|
|
|
|
// Device turns in arrival order, one batch each. A plain mutex lets the
|
|
// thread that just released it take it straight back, so a long request
|
|
// would still starve the requests queued behind it.
|
|
std::mutex turn_mutex;
|
|
std::condition_variable turn_changed;
|
|
std::uint64_t next_ticket{0};
|
|
std::uint64_t serving{0};
|
|
|
|
class Turn {
|
|
public:
|
|
explicit Turn(Impl& impl) : impl_(impl) {
|
|
std::unique_lock lock(impl_.turn_mutex);
|
|
const std::uint64_t ticket = impl_.next_ticket++;
|
|
impl_.turn_changed.wait(lock, [&] { return impl_.serving == ticket; });
|
|
}
|
|
~Turn() {
|
|
{
|
|
const std::lock_guard lock(impl_.turn_mutex);
|
|
++impl_.serving;
|
|
}
|
|
impl_.turn_changed.notify_all();
|
|
}
|
|
Turn(const Turn&) = delete;
|
|
Turn& operator=(const Turn&) = delete;
|
|
Turn(Turn&&) = delete;
|
|
Turn& operator=(Turn&&) = delete;
|
|
|
|
private:
|
|
Impl& impl_;
|
|
};
|
|
|
|
~Impl() {
|
|
if (blas != nullptr) {
|
|
gufo::hip::LogCleanupError(hipblasDestroy(blas));
|
|
}
|
|
if (stream != nullptr) {
|
|
gufo::hip::LogCleanupError(hipStreamDestroy(stream));
|
|
}
|
|
}
|
|
|
|
void Upload();
|
|
void LoadVision();
|
|
void RunVision(const ImagePatches& image, std::size_t offset);
|
|
void Allocate();
|
|
// Mirrors the text server's probe: after a reset even a 4-byte memset
|
|
// fails, while an ordinary kernel or library failure leaves it working.
|
|
[[nodiscard]] bool DeviceUsable();
|
|
|
|
/// Runs one sequence for every GEMM row bucket, so hipBLASLt has tuned all
|
|
/// of them and every kernel is loaded before the first request.
|
|
void Warmup();
|
|
|
|
/// Y[tokens, m] = X[tokens, k] * W[m, k]^T through the tuned hipBLASLt
|
|
/// plan, or rocBLAS when hipBLASLt has none for the shape.
|
|
void Gemm(const void* weights, const void* inputs, float* output,
|
|
std::size_t tokens, std::size_t m, std::size_t k) {
|
|
if (!gemm_lt.Run(weights, inputs, output, tokens, m, k, kChunkTokens,
|
|
stream)) {
|
|
LaunchGemmBf16(blas, weights, inputs, output, tokens, m, k, stream);
|
|
}
|
|
}
|
|
|
|
void RunBatch(const std::vector<EncoderSequence>& sequences,
|
|
std::size_t first, std::size_t last, std::size_t tokens,
|
|
std::vector<std::vector<float>>* embeddings);
|
|
};
|
|
|
|
void EncoderRuntime::Impl::Upload() {
|
|
const TensorStore& store = *loaded.store;
|
|
const std::string prefix = kTextTensorPrefix;
|
|
const auto upload = [&](const std::string& name) {
|
|
const Tensor* tensor = store.Find(name);
|
|
DeviceBuffer buffer(tensor->byte_count);
|
|
buffer.Upload(0, tensor->data, tensor->byte_count);
|
|
return buffer;
|
|
};
|
|
embed_table = upload(prefix + "embed_tokens.weight");
|
|
final_norm = upload(prefix + "norm.weight");
|
|
embedding_projection = upload(prefix + "embedding_projection.weight");
|
|
ple_projection = upload(prefix + "ple.per_layer_model_projection.weight");
|
|
ple_norm = upload(prefix + "ple.per_layer_projection_norm.weight");
|
|
|
|
const TextConfig& text = loaded.config.text;
|
|
for (const LayerGeometry& geometry : text.layers) {
|
|
const auto key = std::make_pair(geometry.head_dim, geometry.rope_theta);
|
|
if (!rope.contains(key)) {
|
|
rope.emplace(key, BuildRopeTable(geometry.head_dim, geometry.rope_theta,
|
|
kMaxSequenceTokens));
|
|
}
|
|
}
|
|
layers.resize(text.num_hidden_layers);
|
|
for (std::size_t index = 0; index < layers.size(); ++index) {
|
|
const std::string p = prefix + "layers." + std::to_string(index) + ".";
|
|
LayerWeights& layer = layers[index];
|
|
layer.input_norm = upload(p + "input_layernorm.weight");
|
|
layer.post_attention_norm = upload(p + "post_attention_layernorm.weight");
|
|
layer.pre_feedforward_norm = upload(p + "pre_feedforward_layernorm.weight");
|
|
layer.post_feedforward_norm =
|
|
upload(p + "post_feedforward_layernorm.weight");
|
|
layer.layer_scalar = upload(p + "layer_scalar");
|
|
layer.qkv = UploadConcatenated(
|
|
store, {p + "self_attn.q_proj.weight", p + "self_attn.k_proj.weight",
|
|
p + "self_attn.v_proj.weight"});
|
|
layer.output = upload(p + "self_attn.o_proj.weight");
|
|
layer.query_norm = upload(p + "self_attn.q_norm.weight");
|
|
layer.key_norm = upload(p + "self_attn.k_norm.weight");
|
|
layer.gate_up = UploadConcatenated(
|
|
store, {p + "mlp.gate_proj.weight", p + "mlp.up_proj.weight"});
|
|
layer.down = upload(p + "mlp.down_proj.weight");
|
|
layer.ple_gate = upload(p + "ple_block.per_layer_input_gate.weight");
|
|
layer.ple_projection = upload(p + "ple_block.per_layer_projection.weight");
|
|
layer.ple_post_norm =
|
|
upload(p + "ple_block.post_per_layer_input_norm.weight");
|
|
}
|
|
}
|
|
|
|
void EncoderRuntime::Impl::LoadVision() {
|
|
if (vision)
|
|
return;
|
|
std::string processor_error;
|
|
if (!ValidateImageProcessor(model_dir, &processor_error))
|
|
throw std::runtime_error(processor_error);
|
|
auto weights = LoadModelDirectory(model_dir, true);
|
|
if (!weights.ok)
|
|
throw std::runtime_error(weights.error);
|
|
auto candidate = std::make_unique<Vision>();
|
|
const auto upload = [&](const std::string& name) {
|
|
const auto* tensor = weights.store->Find(name);
|
|
DeviceBuffer buffer(tensor->byte_count);
|
|
buffer.Upload(0, tensor->data, tensor->byte_count);
|
|
return buffer;
|
|
};
|
|
candidate->input = upload("vision_tower.patch_embedder.input_proj.weight");
|
|
candidate->position_table =
|
|
upload("vision_tower.patch_embedder.position_embedding_table");
|
|
candidate->projection = upload("embed_vision.embedding_projection.weight");
|
|
candidate->layers.resize(16);
|
|
for (unsigned i = 0; i < 16; ++i) {
|
|
auto& layer = candidate->layers[i];
|
|
const std::string p =
|
|
"vision_tower.encoder.layers." + std::to_string(i) + ".";
|
|
layer.input_norm = upload(p + "input_layernorm.weight");
|
|
layer.post_attention_norm = upload(p + "post_attention_layernorm.weight");
|
|
layer.pre_feedforward_norm = upload(p + "pre_feedforward_layernorm.weight");
|
|
layer.post_feedforward_norm =
|
|
upload(p + "post_feedforward_layernorm.weight");
|
|
layer.qkv = UploadConcatenated(*weights.store,
|
|
{p + "self_attn.q_proj.linear.weight",
|
|
p + "self_attn.k_proj.linear.weight",
|
|
p + "self_attn.v_proj.linear.weight"});
|
|
layer.output = upload(p + "self_attn.o_proj.linear.weight");
|
|
layer.query_norm = upload(p + "self_attn.q_norm.weight");
|
|
layer.key_norm = upload(p + "self_attn.k_norm.weight");
|
|
layer.gate_up = UploadConcatenated(
|
|
*weights.store,
|
|
{p + "mlp.gate_proj.linear.weight", p + "mlp.up_proj.linear.weight"});
|
|
layer.down = upload(p + "mlp.down_proj.linear.weight");
|
|
}
|
|
constexpr std::size_t n = 2520, h = 768;
|
|
candidate->pixels = DeviceBuffer(n * h * sizeof(float));
|
|
candidate->pixels_bf16 = DeviceBuffer(n * h * 2);
|
|
candidate->state = DeviceBuffer(n * h * sizeof(float));
|
|
candidate->normed = DeviceBuffer(n * h * 2);
|
|
candidate->qkv = DeviceBuffer(n * h * 3 * sizeof(float));
|
|
candidate->query = DeviceBuffer(n * h * 2);
|
|
candidate->key = DeviceBuffer(n * h * 2);
|
|
candidate->value = DeviceBuffer(n * h * 2);
|
|
candidate->context = DeviceBuffer(n * h * 2);
|
|
candidate->temp = DeviceBuffer(n * h * sizeof(float));
|
|
candidate->gate_up =
|
|
DeviceBuffer(std::min(n, kChunkTokens) * 6144 * sizeof(float));
|
|
candidate->act = DeviceBuffer(n * 3072 * 2);
|
|
candidate->pooled = DeviceBuffer(280 * h * 2);
|
|
candidate->features = DeviceBuffer(280 * 512 * sizeof(float));
|
|
vision = std::move(candidate);
|
|
}
|
|
|
|
void EncoderRuntime::Impl::RunVision(const ImagePatches& image,
|
|
std::size_t offset) {
|
|
LoadVision();
|
|
auto& v = *vision;
|
|
const std::size_t n = std::size_t{image.rows} * image.columns;
|
|
v.pixels.UploadAsync(0, image.pixels.data(),
|
|
image.pixels.size() * sizeof(float), stream);
|
|
LaunchVisionPixels(v.pixels.get<float>(), v.pixels_bf16.get(), n * 768,
|
|
stream);
|
|
LaunchGemmBf16(blas, v.input.get(), v.pixels_bf16.get(), v.temp.get<float>(),
|
|
n, 768, 768, stream);
|
|
LaunchVisionPositions(v.temp.get<float>(), v.position_table.get(),
|
|
v.state.get<float>(), n, image.columns, stream);
|
|
for (const auto& layer : v.layers) {
|
|
LaunchRmsNormToBf16(v.state.get<float>(), layer.input_norm.get(),
|
|
v.normed.get(), n, 768, 1e-6F, stream);
|
|
LaunchGemmBf16(blas, layer.qkv.get(), v.normed.get(), v.qkv.get<float>(), n,
|
|
2304, 768, stream);
|
|
LaunchVisionQkv(v.qkv.get<float>(), layer.query_norm.get(),
|
|
layer.key_norm.get(), v.query.get(), v.key.get(),
|
|
v.value.get(), n, image.columns, stream);
|
|
LaunchVisionAttention(v.query.get(), v.key.get(), v.value.get(),
|
|
v.context.get(), n, stream);
|
|
LaunchGemmBf16(blas, layer.output.get(), v.context.get(),
|
|
v.temp.get<float>(), n, 768, 768, stream);
|
|
LaunchRmsNormResidual(v.temp.get<float>(), layer.post_attention_norm.get(),
|
|
v.state.get<float>(), n, 768, 1e-6F, stream);
|
|
LaunchRmsNormToBf16(v.state.get<float>(), layer.pre_feedforward_norm.get(),
|
|
v.normed.get(), n, 768, 1e-6F, stream);
|
|
for (std::size_t row = 0; row < n; row += kChunkTokens) {
|
|
const std::size_t count = std::min(kChunkTokens, n - row);
|
|
LaunchGemmBf16(blas, layer.gate_up.get(),
|
|
v.normed.get<std::uint16_t>() + row * 768,
|
|
v.gate_up.get<float>(), count, 6144, 768, stream);
|
|
LaunchGeglu(v.gate_up.get<float>(),
|
|
v.act.get<std::uint16_t>() + row * 3072, count, 3072, stream);
|
|
}
|
|
LaunchGemmBf16(blas, layer.down.get(), v.act.get(), v.temp.get<float>(), n,
|
|
768, 3072, stream);
|
|
LaunchRmsNormResidual(v.temp.get<float>(),
|
|
layer.post_feedforward_norm.get(),
|
|
v.state.get<float>(), n, 768, 1e-6F, stream);
|
|
}
|
|
LaunchVisionPool(v.state.get<float>(), v.pooled.get(), image.rows,
|
|
image.columns, stream);
|
|
LaunchGemmBf16(blas, v.projection.get(), v.pooled.get(),
|
|
v.features.get<float>(), image.soft_tokens(), 512, 768,
|
|
stream);
|
|
LaunchVisionInsert(v.features.get<float>(), state.get<float>() + offset * 512,
|
|
embeddings_bf16.get<std::uint16_t>() + offset * 512,
|
|
image.soft_tokens(), stream);
|
|
HIP_CHECK(hipGetLastError());
|
|
}
|
|
|
|
void EncoderRuntime::Impl::Allocate() {
|
|
const TextConfig& text = loaded.config.text;
|
|
std::size_t qkv_width = 0;
|
|
std::size_t query_width = 0;
|
|
std::size_t key_width = 0;
|
|
for (const LayerGeometry& layer : text.layers) {
|
|
query_width = std::max<std::size_t>(
|
|
query_width, std::size_t{text.num_attention_heads} * layer.head_dim);
|
|
key_width = std::max<std::size_t>(
|
|
key_width, std::size_t{layer.num_key_value_heads} * layer.head_dim);
|
|
qkv_width = std::max(qkv_width, query_width + 2U * key_width);
|
|
}
|
|
const std::size_t n = kMaxSequenceTokens;
|
|
const std::size_t hidden = text.hidden_size;
|
|
const std::size_t per_layer = text.hidden_size_per_layer_input;
|
|
token_ids = DeviceBuffer(n * sizeof(std::uint32_t));
|
|
positions = DeviceBuffer(n * sizeof(std::uint32_t));
|
|
sequence_begin = DeviceBuffer(n * sizeof(std::uint32_t));
|
|
sequence_end = DeviceBuffer(n * sizeof(std::uint32_t));
|
|
offsets = DeviceBuffer((n + 1U) * sizeof(std::uint32_t));
|
|
state = DeviceBuffer(n * hidden * sizeof(float));
|
|
embeddings_bf16 = DeviceBuffer(n * hidden * 2U);
|
|
normed_bf16 = DeviceBuffer(n * hidden * 2U);
|
|
// Only one chunk of rows is live at a time.
|
|
qkv_f32 = DeviceBuffer(std::min(n, kChunkTokens) * qkv_width * sizeof(float));
|
|
query_bf16 = DeviceBuffer(n * query_width * 2U);
|
|
key_bf16 = DeviceBuffer(n * key_width * 2U);
|
|
// The transposed copy the attention kernel reads has a stride that is a
|
|
// multiple of 32 tokens. The transpose zero-fills the tail of each column.
|
|
value_bf16 = DeviceBuffer(n * key_width * 2U);
|
|
value_t_bf16 = DeviceBuffer(ValueStride(n) * key_width * 2U);
|
|
context_bf16 = DeviceBuffer(n * query_width * 2U);
|
|
temp_f32 = DeviceBuffer(n * hidden * sizeof(float));
|
|
gate_up_f32 = DeviceBuffer(std::min(n, kChunkTokens) * 2U *
|
|
text.intermediate_size * sizeof(float));
|
|
act_bf16 = DeviceBuffer(n * text.intermediate_size * 2U);
|
|
ple_f32 = DeviceBuffer(n * per_layer * sizeof(float));
|
|
gate_f32 = DeviceBuffer(n * per_layer * sizeof(float));
|
|
projected_f32 = DeviceBuffer(n * text.embedding_dim * sizeof(float));
|
|
pooled_f32 = DeviceBuffer(n * text.embedding_dim * sizeof(float));
|
|
}
|
|
|
|
void EncoderRuntime::Impl::Warmup() {
|
|
std::vector<std::vector<float>> unused;
|
|
std::size_t rows = 0;
|
|
do {
|
|
rows = GemmLt::Bucket(rows + 1, kChunkTokens);
|
|
std::vector<std::uint32_t> filler(rows, 1000);
|
|
unused.clear();
|
|
RunBatch({{std::move(filler), {}}}, 0, 1, rows, &unused);
|
|
} while (rows < kChunkTokens);
|
|
}
|
|
|
|
void EncoderRuntime::Impl::RunBatch(
|
|
const std::vector<EncoderSequence>& sequences, std::size_t first,
|
|
std::size_t last, std::size_t tokens,
|
|
std::vector<std::vector<float>>* embeddings) {
|
|
const TextConfig& text = loaded.config.text;
|
|
const std::size_t hidden = text.hidden_size;
|
|
const std::size_t per_layer = text.hidden_size_per_layer_input;
|
|
const float epsilon = text.rms_norm_eps;
|
|
|
|
// Pack the sequences and describe each token's sequence bounds.
|
|
host_ids.clear();
|
|
host_positions.clear();
|
|
host_begin.clear();
|
|
host_end.clear();
|
|
host_offsets.assign(1, 0);
|
|
host_ids.reserve(tokens);
|
|
for (std::size_t s = first; s < last; ++s) {
|
|
const auto begin = static_cast<std::uint32_t>(host_ids.size());
|
|
const auto end =
|
|
begin + static_cast<std::uint32_t>(sequences[s].tokens.size());
|
|
for (std::size_t t = 0; t < sequences[s].tokens.size(); ++t) {
|
|
host_ids.push_back(sequences[s].tokens[t]);
|
|
host_positions.push_back(static_cast<std::uint32_t>(t));
|
|
host_begin.push_back(begin);
|
|
host_end.push_back(end);
|
|
}
|
|
host_offsets.push_back(end);
|
|
}
|
|
token_ids.UploadAsync(0, host_ids.data(), tokens * sizeof(std::uint32_t),
|
|
stream);
|
|
positions.UploadAsync(0, host_positions.data(),
|
|
tokens * sizeof(std::uint32_t), stream);
|
|
sequence_begin.UploadAsync(0, host_begin.data(),
|
|
tokens * sizeof(std::uint32_t), stream);
|
|
sequence_end.UploadAsync(0, host_end.data(), tokens * sizeof(std::uint32_t),
|
|
stream);
|
|
offsets.UploadAsync(0, host_offsets.data(),
|
|
host_offsets.size() * sizeof(std::uint32_t), stream);
|
|
|
|
LaunchEmbeddingLookup(embed_table.get(), token_ids.get<std::uint32_t>(),
|
|
state.get<float>(), embeddings_bf16.get(), tokens,
|
|
hidden, kEmbedScale, stream);
|
|
|
|
for (std::size_t s = first; s < last; ++s) {
|
|
for (const auto& image : sequences[s].images) {
|
|
RunVision(image.patches, host_offsets[s - first] + image.offset);
|
|
}
|
|
}
|
|
|
|
const float ple_scale = 1.0F / std::sqrt(static_cast<float>(hidden));
|
|
for (std::size_t index = 0; index < layers.size(); ++index) {
|
|
const LayerWeights& layer = layers[index];
|
|
const LayerGeometry& geometry = text.layers[index];
|
|
const QkvLayout layout{text.num_attention_heads,
|
|
geometry.num_key_value_heads, geometry.head_dim};
|
|
const std::size_t query_width =
|
|
std::size_t{layout.num_heads} * layout.head_dim;
|
|
const std::size_t key_width =
|
|
std::size_t{layout.num_key_value_heads} * layout.head_dim;
|
|
const bool full = geometry.type == LayerType::kFullAttention;
|
|
const RopeTable& rope_table =
|
|
rope.at(std::make_pair(geometry.head_dim, geometry.rope_theta));
|
|
|
|
LaunchRmsNormToBf16(state.get<float>(), layer.input_norm.get(),
|
|
normed_bf16.get(), tokens, hidden, epsilon, stream);
|
|
for (std::size_t row = 0; row < tokens; row += kChunkTokens) {
|
|
const std::size_t rows = std::min(kChunkTokens, tokens - row);
|
|
Gemm(layer.qkv.get(), normed_bf16.get<std::uint16_t>() + row * hidden,
|
|
qkv_f32.get<float>(), rows, query_width + 2U * key_width, hidden);
|
|
LaunchQkvNormRope(qkv_f32.get<float>(), layer.query_norm.get(),
|
|
layer.key_norm.get(),
|
|
query_bf16.get<std::uint16_t>() + row * query_width,
|
|
key_bf16.get<std::uint16_t>() + row * key_width,
|
|
value_bf16.get<std::uint16_t>() + row * key_width,
|
|
positions.get<std::uint32_t>() + row, rows, layout,
|
|
rope_table.cos.get<float>(),
|
|
rope_table.sin.get<float>(), epsilon, stream);
|
|
}
|
|
LaunchTransposeValues(value_bf16.get(), value_t_bf16.get(), tokens,
|
|
static_cast<std::uint32_t>(key_width), stream);
|
|
if (!LaunchAttention(query_bf16.get(), key_bf16.get(), value_t_bf16.get(),
|
|
context_bf16.get(),
|
|
sequence_begin.get<std::uint32_t>(),
|
|
sequence_end.get<std::uint32_t>(), tokens, layout,
|
|
full ? 0U : text.sliding_window, stream)) {
|
|
throw std::runtime_error("EmbeddingGemma 2 attention geometry");
|
|
}
|
|
for (std::size_t row = 0; row < tokens; row += kChunkTokens) {
|
|
const std::size_t rows = std::min(kChunkTokens, tokens - row);
|
|
Gemm(layer.output.get(),
|
|
context_bf16.get<std::uint16_t>() + row * query_width,
|
|
temp_f32.get<float>(), rows, hidden, query_width);
|
|
LaunchRmsNormResidual(
|
|
temp_f32.get<float>(), layer.post_attention_norm.get(),
|
|
state.get<float>() + row * hidden, rows, hidden, epsilon, stream);
|
|
}
|
|
|
|
LaunchRmsNormToBf16(state.get<float>(), layer.pre_feedforward_norm.get(),
|
|
normed_bf16.get(), tokens, hidden, epsilon, stream);
|
|
for (std::size_t row = 0; row < tokens; row += kChunkTokens) {
|
|
const std::size_t rows = std::min(kChunkTokens, tokens - row);
|
|
Gemm(layer.gate_up.get(), normed_bf16.get<std::uint16_t>() + row * hidden,
|
|
gate_up_f32.get<float>(), rows, 2U * text.intermediate_size, hidden);
|
|
LaunchGeglu(gate_up_f32.get<float>(),
|
|
act_bf16.get<std::uint16_t>() + row * text.intermediate_size,
|
|
rows, text.intermediate_size, stream);
|
|
}
|
|
for (std::size_t row = 0; row < tokens; row += kChunkTokens) {
|
|
const std::size_t rows = std::min(kChunkTokens, tokens - row);
|
|
Gemm(layer.down.get(),
|
|
act_bf16.get<std::uint16_t>() + row * text.intermediate_size,
|
|
temp_f32.get<float>(), rows, hidden, text.intermediate_size);
|
|
LaunchRmsNormResidual(
|
|
temp_f32.get<float>(), layer.post_feedforward_norm.get(),
|
|
state.get<float>() + row * hidden, rows, hidden, epsilon, stream);
|
|
}
|
|
|
|
// The per-layer input block takes this layer's slice of the per-layer
|
|
// inputs, then runs a gated bottleneck that mixes it into the residual
|
|
// stream.
|
|
for (std::size_t row = 0; row < tokens; row += kChunkTokens) {
|
|
const std::size_t rows = std::min(kChunkTokens, tokens - row);
|
|
float* const state_rows = state.get<float>() + row * hidden;
|
|
std::uint16_t* const normed_rows =
|
|
normed_bf16.get<std::uint16_t>() + row * hidden;
|
|
Gemm(ple_projection.get<std::byte>() + index * per_layer * hidden * 2U,
|
|
embeddings_bf16.get<std::uint16_t>() + row * hidden,
|
|
ple_f32.get<float>(), rows, per_layer, hidden);
|
|
LaunchScaledRmsNorm(ple_f32.get<float>(), ple_norm.get(), rows, per_layer,
|
|
ple_scale, epsilon, stream);
|
|
LaunchFloatToBf16(state_rows, normed_rows, rows * hidden, stream);
|
|
Gemm(layer.ple_gate.get(), normed_rows, gate_f32.get<float>(), rows,
|
|
per_layer, hidden);
|
|
LaunchPleGate(gate_f32.get<float>(), ple_f32.get<float>(), act_bf16.get(),
|
|
rows * per_layer, stream);
|
|
Gemm(layer.ple_projection.get(), act_bf16.get(), temp_f32.get<float>(),
|
|
rows, hidden, per_layer);
|
|
LaunchPleResidual(temp_f32.get<float>(), layer.ple_post_norm.get(),
|
|
layer.layer_scalar.get(), state_rows, rows, hidden,
|
|
epsilon, stream);
|
|
}
|
|
HIP_CHECK(hipGetLastError());
|
|
}
|
|
|
|
LaunchRmsNormToBf16(state.get<float>(), final_norm.get(), normed_bf16.get(),
|
|
tokens, hidden, epsilon, stream);
|
|
for (std::size_t row = 0; row < tokens; row += kChunkTokens) {
|
|
Gemm(embedding_projection.get(),
|
|
normed_bf16.get<std::uint16_t>() + row * hidden,
|
|
projected_f32.get<float>() + row * text.embedding_dim,
|
|
std::min(kChunkTokens, tokens - row), text.embedding_dim, hidden);
|
|
}
|
|
const std::size_t count = last - first;
|
|
if (!LaunchMeanPoolNormalize(
|
|
projected_f32.get<float>(), offsets.get<std::uint32_t>(),
|
|
pooled_f32.get<float>(), count, text.embedding_dim, stream)) {
|
|
throw std::runtime_error("EmbeddingGemma 2 pooling width");
|
|
}
|
|
HIP_CHECK(hipGetLastError());
|
|
|
|
std::vector<float> host(count * text.embedding_dim);
|
|
HIP_CHECK(hipMemcpyAsync(host.data(), pooled_f32.get(),
|
|
host.size() * sizeof(float), hipMemcpyDeviceToHost,
|
|
stream));
|
|
HIP_CHECK(hipStreamSynchronize(stream));
|
|
for (std::size_t s = 0; s < count; ++s) {
|
|
embeddings->emplace_back(
|
|
host.begin() + static_cast<std::ptrdiff_t>(s * text.embedding_dim),
|
|
host.begin() +
|
|
static_cast<std::ptrdiff_t>((s + 1U) * text.embedding_dim));
|
|
}
|
|
}
|
|
|
|
bool EncoderRuntime::Impl::DeviceUsable() {
|
|
(void)hipGetLastError();
|
|
return hipMemsetAsync(probe.get(), 0, probe.bytes(), stream) == hipSuccess &&
|
|
hipStreamSynchronize(stream) == hipSuccess;
|
|
}
|
|
|
|
EncoderRuntime::EncoderRuntime(std::unique_ptr<Impl> impl)
|
|
: impl_(std::move(impl)) {}
|
|
|
|
EncoderRuntime::~EncoderRuntime() = default;
|
|
|
|
std::unique_ptr<EncoderRuntime> EncoderRuntime::Create(
|
|
const std::string& model_dir, std::string* error) {
|
|
const auto fail =
|
|
[&](const std::string& message) -> std::unique_ptr<EncoderRuntime> {
|
|
if (error != nullptr) {
|
|
*error = message;
|
|
}
|
|
return nullptr;
|
|
};
|
|
auto impl = std::make_unique<Impl>();
|
|
try {
|
|
impl->model_dir = model_dir;
|
|
impl->loaded = LoadModelDirectory(model_dir);
|
|
if (!impl->loaded.ok) {
|
|
return fail(impl->loaded.error);
|
|
}
|
|
HIP_CHECK(hipStreamCreateWithFlags(&impl->stream, hipStreamNonBlocking));
|
|
HIPBLAS_CHECK(hipblasCreate(&impl->blas));
|
|
impl->Upload();
|
|
impl->Allocate();
|
|
impl->probe = DeviceBuffer(sizeof(std::uint32_t));
|
|
// The device now holds every weight. Unmapping the checkpoint releases
|
|
// its page-cache residency, about 650 MB of host memory.
|
|
impl->loaded.store.reset();
|
|
impl->loaded.mappings.clear();
|
|
impl->Warmup();
|
|
return std::unique_ptr<EncoderRuntime>(new EncoderRuntime(std::move(impl)));
|
|
} catch (const std::exception& failure) {
|
|
// Warm-up copies may still be queued. They read the host staging vectors
|
|
// that `impl` is about to destroy.
|
|
if (impl != nullptr && impl->stream != nullptr) {
|
|
gufo::hip::LogCleanupError(hipStreamSynchronize(impl->stream));
|
|
}
|
|
return fail(std::string("embedding_gemma2: ") + failure.what());
|
|
}
|
|
}
|
|
|
|
const ModelConfig& EncoderRuntime::config() const {
|
|
return impl_->loaded.config;
|
|
}
|
|
|
|
bool EncoderRuntime::device_lost() const noexcept {
|
|
return impl_->lost.load(std::memory_order_acquire);
|
|
}
|
|
|
|
bool EncoderRuntime::Embed(
|
|
const std::vector<std::vector<std::uint32_t>>& sequences,
|
|
std::vector<std::vector<float>>* embeddings, std::string* error,
|
|
const std::function<bool()>& is_cancelled) {
|
|
std::vector<EncoderSequence> inputs;
|
|
inputs.reserve(sequences.size());
|
|
for (const auto& tokens : sequences)
|
|
inputs.push_back({tokens, {}});
|
|
return EmbedMultimodal(inputs, embeddings, error, is_cancelled);
|
|
}
|
|
|
|
bool EncoderRuntime::EmbedMultimodal(
|
|
const std::vector<EncoderSequence>& sequences,
|
|
std::vector<std::vector<float>>* embeddings, std::string* error,
|
|
const std::function<bool()>& is_cancelled) {
|
|
const auto fail = [&](const std::string& message) {
|
|
if (error != nullptr) {
|
|
*error = message;
|
|
}
|
|
return false;
|
|
};
|
|
const TextConfig& text = impl_->loaded.config.text;
|
|
for (const auto& sequence : sequences) {
|
|
if (sequence.tokens.empty()) {
|
|
return fail("embedding_gemma2: empty token sequence");
|
|
}
|
|
if (sequence.tokens.size() > kMaxSequenceTokens) {
|
|
return fail("embedding_gemma2: sequence exceeds " +
|
|
std::to_string(kMaxSequenceTokens) + " tokens");
|
|
}
|
|
std::size_t previous_end = 0;
|
|
for (const auto& image : sequence.images) {
|
|
const auto& patches = image.patches;
|
|
const std::size_t n = patches.soft_tokens();
|
|
if (patches.rows == 0 || patches.columns == 0 || patches.rows % 3 ||
|
|
patches.columns % 3 || n > 280 ||
|
|
patches.pixels.size() != n * 9 * 768 || image.offset < previous_end ||
|
|
image.offset > sequence.tokens.size() ||
|
|
n > sequence.tokens.size() - image.offset) {
|
|
return fail("embedding_gemma2: invalid image patches or token offset");
|
|
}
|
|
for (std::size_t j = 0; j < n; ++j) {
|
|
if (sequence.tokens[image.offset + j] != kImageTokenId) {
|
|
return fail("embedding_gemma2: image placeholder mismatch");
|
|
}
|
|
}
|
|
previous_end = image.offset + n;
|
|
}
|
|
for (const std::uint32_t id : sequence.tokens) {
|
|
if (id >= text.vocab_size) {
|
|
return fail("embedding_gemma2: token id out of range");
|
|
}
|
|
}
|
|
}
|
|
embeddings->clear();
|
|
embeddings->reserve(sequences.size());
|
|
std::size_t first = 0;
|
|
while (first < sequences.size()) {
|
|
std::size_t last = first;
|
|
std::size_t tokens = 0;
|
|
while (last < sequences.size() &&
|
|
tokens + sequences[last].tokens.size() <= kMaxSequenceTokens) {
|
|
tokens += sequences[last].tokens.size();
|
|
++last;
|
|
}
|
|
// Each batch takes a new turn, so requests that arrived meanwhile run
|
|
// before the next batch of this one.
|
|
const Impl::Turn turn(*impl_);
|
|
if (device_lost()) {
|
|
embeddings->clear();
|
|
return fail(kDeviceLostMessage);
|
|
}
|
|
if (is_cancelled && is_cancelled()) {
|
|
embeddings->clear();
|
|
return fail(kCancelledMessage);
|
|
}
|
|
try {
|
|
impl_->RunBatch(sequences, first, last, tokens, embeddings);
|
|
} catch (const std::exception& failure) {
|
|
// Kernels from the failed batch may still be queued. They read the
|
|
// index buffers the next batch overwrites, so drain the stream before
|
|
// the turn passes on.
|
|
gufo::hip::LogCleanupError(hipStreamSynchronize(impl_->stream));
|
|
embeddings->clear();
|
|
if (!impl_->DeviceUsable()) {
|
|
impl_->lost.store(true, std::memory_order_release);
|
|
return fail(std::string(kDeviceLostMessage) + ": " + failure.what());
|
|
}
|
|
return fail(std::string("embedding_gemma2: ") + failure.what());
|
|
}
|
|
first = last;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace gufo::models::embedding_gemma2::hip
|