Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .devops/nix/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
metalSupport ? stdenv.isDarwin,
rocmSupport ? config.rocmSupport or false,
rocmGpuTargets ? (lib.optionals rocmSupport rocmPackages.clr.gpuTargets),
strixHaloOptimizations ? (rocmSupport && rocmGpuTargets == [ "gfx1151" ]),
# Model selection: if non-empty, only these model targets are built.
# See CMakeLists.txt AUDIOCPP_MODEL_SET / AUDIOCPP_MODELS.
models ? [ ],
Expand Down Expand Up @@ -78,7 +79,8 @@ stdenv.mkDerivation (finalAttrs: {
++ lib.optional metalSupport "-DENGINE_ENABLE_METAL=ON"
++ lib.optional rocmSupport "-DENGINE_ENABLE_HIP=ON"
++ lib.optional rocmSupport "-DCMAKE_HIP_COMPILER=${rocmPackages.llvm.clang}/bin/clang"
++ lib.optional rocmSupport "-DGPU_TARGETS=${lib.concatStringsSep ";" rocmGpuTargets}";
++ lib.optional rocmSupport "-DGPU_TARGETS=${lib.concatStringsSep ";" rocmGpuTargets}"
++ lib.optional strixHaloOptimizations "-DENGINE_HIP_STRIX_HALO_OPTIMIZATIONS=ON";

env = lib.optionalAttrs rocmSupport {
ROCM_PATH = "${rocmPackages.clr}";
Expand Down
51 changes: 51 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ option(ENGINE_ENABLE_VULKAN "Build ggml with Vulkan backend support" ${ENGINE_DE
option(ENGINE_ENABLE_METAL "Build ggml with Metal backend support" ${ENGINE_DEFAULT_ENABLE_METAL})
option(ENGINE_ENABLE_LLAMAFILE "Build ggml with llamafile SGEMM support" ${ENGINE_DEFAULT_ENABLE_LLAMAFILE})
option(ENGINE_ENABLE_CUDA_GRAPHS "Enable ggml CUDA graphs support" ${ENGINE_DEFAULT_ENABLE_CUDA_GRAPHS})
option(ENGINE_HIP_STRIX_HALO_OPTIMIZATIONS
"Enable opt-in HIP optimizations validated only for Strix Halo (gfx1151)"
OFF)
option(ENGINE_ENABLE_NATIVE_CPU "Build ggml CPU kernels with native host ISA flags" ${ENGINE_DEFAULT_ENABLE_NATIVE_CPU})
option(ENGINE_ENABLE_OPENMP "Build host code with OpenMP support" ON)
option(ENGINE_ENABLE_CPU_ALL_VARIANTS
Expand All @@ -105,6 +108,47 @@ if (ENGINE_ENABLE_CUDA AND ENGINE_ENABLE_HIP)
"configure with exactly one of them (e.g. -DENGINE_ENABLE_CUDA=OFF -DENGINE_ENABLE_HIP=ON)")
endif()

set(AUDIOCPP_HIP_STRIX_HALO_OPTIMIZATIONS_ACTIVE OFF)
if (ENGINE_HIP_STRIX_HALO_OPTIMIZATIONS)
if (NOT ENGINE_ENABLE_HIP)
message(FATAL_ERROR
"ENGINE_HIP_STRIX_HALO_OPTIMIZATIONS=ON requires ENGINE_ENABLE_HIP=ON")
endif()

# Match ggml HIP target precedence: an explicit CMake HIP architecture wins,
# followed by GPU_TARGETS and then the legacy AMDGPU_TARGETS spelling.
if (CMAKE_HIP_ARCHITECTURES)
set(AUDIOCPP_EFFECTIVE_HIP_TARGETS ${CMAKE_HIP_ARCHITECTURES})
set(AUDIOCPP_EFFECTIVE_HIP_TARGET_SOURCE "CMAKE_HIP_ARCHITECTURES")
elseif (GPU_TARGETS)
set(AUDIOCPP_EFFECTIVE_HIP_TARGETS ${GPU_TARGETS})
set(AUDIOCPP_EFFECTIVE_HIP_TARGET_SOURCE "GPU_TARGETS")
elseif (AMDGPU_TARGETS)
set(AUDIOCPP_EFFECTIVE_HIP_TARGETS ${AMDGPU_TARGETS})
set(AUDIOCPP_EFFECTIVE_HIP_TARGET_SOURCE "AMDGPU_TARGETS")
else()
message(FATAL_ERROR
"ENGINE_HIP_STRIX_HALO_OPTIMIZATIONS=ON requires exactly one explicit HIP target, gfx1151; "
"set -DGPU_TARGETS=gfx1151 (no project-wide gfx1151 default is provided)")
endif()

list(LENGTH AUDIOCPP_EFFECTIVE_HIP_TARGETS AUDIOCPP_EFFECTIVE_HIP_TARGET_COUNT)
if (NOT AUDIOCPP_EFFECTIVE_HIP_TARGET_COUNT EQUAL 1 OR
NOT "${AUDIOCPP_EFFECTIVE_HIP_TARGETS}" STREQUAL "gfx1151")
message(FATAL_ERROR
"ENGINE_HIP_STRIX_HALO_OPTIMIZATIONS=ON requires the effective HIP target list to be exactly "
"gfx1151; ${AUDIOCPP_EFFECTIVE_HIP_TARGET_SOURCE}='${AUDIOCPP_EFFECTIVE_HIP_TARGETS}'")
endif()

# hipBLASLt regressed decoder compute by roughly 3 seconds on gfx1151.
# Keep the validated retained-graph path on rocBLAS; the generic HIP build
# remains unchanged because this option is both target- and backend-gated.
set(GGML_HIP_HIPBLASLT OFF CACHE BOOL
"Use hipBLASLt instead of hipBLAS (rocBLAS) for HIP GEMM" FORCE)
set(AUDIOCPP_HIP_STRIX_HALO_OPTIMIZATIONS_ACTIVE ON)
message(STATUS "Strix Halo HIP optimizations enabled for gfx1151 with rocBLAS")
endif()

if (ENGINE_ENABLE_CUDA AND NOT ENGINE_ENABLE_HIP)
enable_language(CUDA)
# 12.0 is a floor, not a target: any 12.x or 13.x works (the Dockerfile pins
Expand Down Expand Up @@ -198,6 +242,9 @@ function(audiocpp_configure_runtime_object target_name)
${CMAKE_CURRENT_SOURCE_DIR}/external/libyaml/include
)
target_compile_definitions(${target_name} PRIVATE ${AUDIOCPP_LIBYAML_COMPILE_DEFINITIONS})
if (AUDIOCPP_HIP_STRIX_HALO_OPTIMIZATIONS_ACTIVE)
target_compile_definitions(${target_name} PRIVATE ENGINE_HIP_STRIX_HALO_OPTIMIZATIONS=1)
endif()
target_link_libraries(${target_name} PRIVATE ggml sentencepiece)
if (ENGINE_ENABLE_OPENMP)
target_link_libraries(${target_name} PRIVATE OpenMP::OpenMP_CXX)
Expand Down Expand Up @@ -1166,6 +1213,9 @@ target_include_directories(engine_runtime PRIVATE

target_link_libraries(engine_runtime PUBLIC ggml)
target_link_libraries(engine_runtime PRIVATE sentencepiece cjson_vendor yaml_vendor)
if (AUDIOCPP_HIP_STRIX_HALO_OPTIMIZATIONS_ACTIVE)
target_compile_definitions(engine_runtime PRIVATE ENGINE_HIP_STRIX_HALO_OPTIMIZATIONS=1)
endif()
if (ENGINE_ENABLE_OPENMP)
target_link_libraries(engine_runtime PRIVATE OpenMP::OpenMP_CXX)
if (MSVC)
Expand Down Expand Up @@ -1522,6 +1572,7 @@ if (ENGINE_BUILD_TESTS)
COMMAND outetts_generation_budget_test
)


add_engine_unittest(subtitle_formatter_test tests/unittests/test_subtitle_formatter.cpp)

add_test(
Expand Down
11 changes: 11 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
rocm-gfx1151 = base.override {
rocmSupport = true;
rocmGpuTargets = [ "gfx1151" ];
strixHaloOptimizations = true;
};
}
// nixpkgs.lib.optionalAttrs pkgs.${system}.stdenv.isDarwin {
Expand All @@ -96,6 +97,16 @@
inputsFrom = [ self.packages.${system}.cuda ];
};
}
# ROCm's Nix toolchain is currently supported on x86_64 Linux only.
# Keep the shell off aarch64 rather than exposing an unevaluable output.
// nixpkgs.lib.optionalAttrs (system == "x86_64-linux") {
rocm = pkgs.${system}.mkShell {
inputsFrom = [ self.packages.${system}.rocm ];
};
rocm-gfx1151 = pkgs.${system}.mkShell {
inputsFrom = [ self.packages.${system}.rocm-gfx1151 ];
};
}
);
};
}
6 changes: 6 additions & 0 deletions include/engine/framework/core/module.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ enum class BackendType {
BestAvailable,
};

// CUDA and HIP share ggml's ggml-cuda implementation for a small set of
// explicitly verified operators. This is intentionally not a generic GPU test.
constexpr bool uses_ggml_cuda_or_hip_backend(BackendType type) noexcept {
return type == BackendType::Cuda || type == BackendType::Hip;
}

constexpr size_t kMaxTensorRank = 4;

struct TensorShape {
Expand Down
4 changes: 4 additions & 0 deletions include/engine/models/qwen3_tts/tokenizer_speech_decoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "engine/models/qwen3_tts/assets.h"
#include "engine/models/qwen3_tts/types.h"

#include <array>
#include <cstddef>
#include <memory>

Expand Down Expand Up @@ -45,6 +46,9 @@ class Qwen3SpeechTokenizerDecoderRuntime {
Qwen3TTSPerfMode perf_mode_ = Qwen3TTSPerfMode::Standard;
std::unique_ptr<core::ConstantTensorCache> constants_;
mutable std::unique_ptr<Qwen3SpeechTokenizerDecoderGraph> graph_;
// Always present to keep this public class layout identical when the private
// Strix Halo compile definition differs between translation units.
mutable std::array<std::unique_ptr<Qwen3SpeechTokenizerDecoderGraph>, 2> optimized_graphs_;
};

} // namespace qwen3_tts
Expand Down
3 changes: 1 addition & 2 deletions src/framework/modules/conv_modules.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,7 @@ core::TensorValue view_batch_matrix(
bool is_conv_transpose1d_col2im_fast_path_eligible(
const core::ModuleBuildContext & ctx,
const ConvTranspose1dConfig & config) noexcept {
return (ctx.backend_type == core::BackendType::Cuda ||
ctx.backend_type == core::BackendType::Hip ||
return (core::uses_ggml_cuda_or_hip_backend(ctx.backend_type) ||
ctx.backend_type == core::BackendType::Metal) &&
config.dilation == 1;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ core::TensorValue FastPackedProjection4Module::build(
if (ctx.ggml == nullptr) {
throw std::runtime_error("ModuleBuildContext.ggml is null");
}
if (ctx.backend_type != core::BackendType::Cuda) {
throw std::runtime_error("FastPackedProjection4Module is CUDA-only");
if (!core::uses_ggml_cuda_or_hip_backend(ctx.backend_type)) {
throw std::runtime_error("FastPackedProjection4Module requires CUDA or HIP backend");
}

core::validate_rank_between(input, 1, core::kMaxTensorRank, "input");
Expand Down
107 changes: 84 additions & 23 deletions src/models/qwen3_tts/tokenizer_speech_decoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <limits>
#include <memory>
#include <optional>
#include <stdexcept>
Expand All @@ -45,6 +46,12 @@ constexpr int64_t kSampleRate = 24000;
constexpr int64_t kDecodeSamplesPerCode = 1920;
constexpr int64_t kChunkCodes = 300;
constexpr int64_t kLeftContextCodes = 25;
constexpr std::array<int64_t, 2> kStrixHaloCachedChunkFrames{300, 105};
#if defined(ENGINE_HIP_STRIX_HALO_OPTIMIZATIONS)
constexpr bool kStrixHaloGraphCacheEnabled = true;
#else
constexpr bool kStrixHaloGraphCacheEnabled = false;
#endif
constexpr float kCodebookEps = 1.0e-5F;
constexpr float kMaskNegInf = -1.0e9F;

Expand Down Expand Up @@ -1018,21 +1025,20 @@ class Qwen3SpeechTokenizerDecoderGraph {
if (gallocr_ == nullptr || !ggml_gallocr_alloc_graph(gallocr_, graph_)) {
throw std::runtime_error("failed to allocate Qwen3 speech decoder graph");
}
std::vector<int32_t> positions(static_cast<size_t>(code_frames_));
positions_data_.resize(static_cast<size_t>(code_frames_));
for (int64_t i = 0; i < code_frames_; ++i) {
positions[static_cast<size_t>(i)] = static_cast<int32_t>(i);
positions_data_[static_cast<size_t>(i)] = static_cast<int32_t>(i);
}
const auto mask = make_mask(code_frames_, config.sliding_window);
ggml_backend_tensor_set(positions_, positions.data(), 0, positions.size() * sizeof(int32_t));
if (perf_mode_ == Qwen3TTSPerfMode::FlashAttention) {
std::vector<ggml_fp16_t> mask_f16(mask.size());
mask_f16_data_.resize(mask.size());
for (size_t index = 0; index < mask.size(); ++index) {
mask_f16[index] = ggml_fp32_to_fp16(mask[index]);
mask_f16_data_[index] = ggml_fp32_to_fp16(mask[index]);
}
ggml_backend_tensor_set(mask_, mask_f16.data(), 0, mask_f16.size() * sizeof(ggml_fp16_t));
} else {
ggml_backend_tensor_set(mask_, mask.data(), 0, mask.size() * sizeof(float));
mask_f32_data_ = mask;
}
upload_static_inputs();
}

~Qwen3SpeechTokenizerDecoderGraph() {
Expand All @@ -1059,6 +1065,10 @@ class Qwen3SpeechTokenizerDecoderGraph {
throw std::runtime_error("Qwen3 speech decoder code count exceeds graph capacity");
}
const auto upload_start = Clock::now();
// Cached GGML graphs may reuse backend allocations whose input contents are
// not guaranteed to survive a prior execution. Restore every declared input,
// not only the request-varying codes, before replaying a retained graph.
upload_static_inputs();
std::vector<int32_t> tensor_codes(expected, 0);
for (int64_t frame = 0; frame < input_frames; ++frame) {
for (int64_t group = 0; group < weights_->config.num_quantizers; ++group) {
Expand All @@ -1071,7 +1081,6 @@ class Qwen3SpeechTokenizerDecoderGraph {
const auto compute_start = Clock::now();
core::set_backend_threads(backend_, compute_threads_);
const ggml_status status = engine::core::compute_backend_graph(backend_, graph_);
ggml_backend_synchronize(backend_);
last_graph_compute_ms_ = engine::debug::elapsed_ms(compute_start, Clock::now());
if (status != GGML_STATUS_SUCCESS) {
throw std::runtime_error("Qwen3 speech decoder graph compute failed");
Expand All @@ -1096,6 +1105,27 @@ class Qwen3SpeechTokenizerDecoderGraph {
}

private:
void upload_static_inputs() {
ggml_backend_tensor_set(
positions_,
positions_data_.data(),
0,
positions_data_.size() * sizeof(int32_t));
if (perf_mode_ == Qwen3TTSPerfMode::FlashAttention) {
ggml_backend_tensor_set(
mask_,
mask_f16_data_.data(),
0,
mask_f16_data_.size() * sizeof(ggml_fp16_t));
} else {
ggml_backend_tensor_set(
mask_,
mask_f32_data_.data(),
0,
mask_f32_data_.size() * sizeof(float));
}
}

std::shared_ptr<const Qwen3SpeechTokenizerDecoderWeights> weights_;
int64_t code_frames_ = 0;
int64_t waveform_frames_ = 0;
Expand All @@ -1106,6 +1136,9 @@ class Qwen3SpeechTokenizerDecoderGraph {
ggml_tensor * positions_ = nullptr;
ggml_tensor * mask_ = nullptr;
ggml_tensor * output_ = nullptr;
std::vector<int32_t> positions_data_;
std::vector<float> mask_f32_data_;
std::vector<ggml_fp16_t> mask_f16_data_;
ggml_cgraph * graph_ = nullptr;
ggml_gallocr_t gallocr_ = nullptr;
Qwen3TTSPerfMode perf_mode_ = Qwen3TTSPerfMode::Standard;
Expand Down Expand Up @@ -1161,6 +1194,8 @@ runtime::AudioBuffer Qwen3SpeechTokenizerDecoderRuntime::decode(const Qwen3Speec
int64_t chunks = 0;
int64_t graph_rebuilds = 0;
int64_t max_chunk_frames = 0;
const bool optimized_cache_enabled =
kStrixHaloGraphCacheEnabled && execution_context_->backend_type() == core::BackendType::Hip;
for (int64_t start = 0; start < codec_codes.frames; start += kChunkCodes) {
const int64_t end = std::min<int64_t>(start + kChunkCodes, codec_codes.frames);
const int64_t context = start > kLeftContextCodes ? kLeftContextCodes : start;
Expand All @@ -1175,23 +1210,37 @@ runtime::AudioBuffer Qwen3SpeechTokenizerDecoderRuntime::decode(const Qwen3Speec
std::copy(src, src + codec_codes.code_groups, dst);
}
const int threads = std::max(1, execution_context_->config().threads);
if (graph_ == nullptr || !graph_->matches(*weights_, chunk_frames, execution_context_->backend(), threads)) {
auto * graph_slot = &graph_;
#if defined(ENGINE_HIP_STRIX_HALO_OPTIMIZATIONS)
if (optimized_cache_enabled) {
for (size_t index = 0; index < kStrixHaloCachedChunkFrames.size(); ++index) {
if (chunk_frames == kStrixHaloCachedChunkFrames[index]) {
graph_slot = &optimized_graphs_[index];
break;
}
}
}
#endif
auto & graph = *graph_slot;
const bool graph_rebuilt =
graph == nullptr || !graph->matches(*weights_, chunk_frames, execution_context_->backend(), threads);
if (graph_rebuilt) {
const auto build_start = Clock::now();
graph_.reset();
graph_ = std::make_unique<Qwen3SpeechTokenizerDecoderGraph>(
auto replacement = std::make_unique<Qwen3SpeechTokenizerDecoderGraph>(
weights_,
chunk_frames,
*execution_context_,
*constants_,
graph_arena_bytes_,
perf_mode_);
graph_build_ms += engine::debug::elapsed_ms(build_start, Clock::now());
graph = std::move(replacement);
++graph_rebuilds;
}
auto decoded = graph_->run(chunk.data(), chunk.size());
input_upload_ms += graph_->last_input_upload_ms();
graph_compute_ms += graph_->last_graph_compute_ms();
output_read_ms += graph_->last_output_read_ms();
auto decoded = graph->run(chunk.data(), chunk.size());
input_upload_ms += graph->last_input_upload_ms();
graph_compute_ms += graph->last_graph_compute_ms();
output_read_ms += graph->last_output_read_ms();
++chunks;
const int64_t drop = context * kDecodeSamplesPerCode;
if (drop > static_cast<int64_t>(decoded.size())) {
Expand Down Expand Up @@ -1221,22 +1270,34 @@ runtime::AudioBuffer Qwen3SpeechTokenizerDecoderRuntime::decode_and_trim_referen
if (reference_codes.code_groups != generated_codes.code_groups) {
throw std::runtime_error("Qwen3 speech decoder reference/generated code group mismatch");
}
if (reference_codes.frames < 0 || generated_codes.frames < 0 || reference_codes.code_groups <= 0) {
throw std::runtime_error("Qwen3 speech decoder reference/generated code shape is invalid");
}
if (reference_codes.frames > std::numeric_limits<int64_t>::max() - generated_codes.frames) {
throw std::runtime_error("Qwen3 speech decoder combined frame count is too large");
}
Qwen3SpeechCodes combined;
combined.frames = reference_codes.frames + generated_codes.frames;
combined.code_groups = reference_codes.code_groups;
combined.codes.reserve(static_cast<size_t>(combined.frames * combined.code_groups));
if (combined.frames > std::numeric_limits<int64_t>::max() / combined.code_groups) {
throw std::runtime_error("Qwen3 speech decoder combined code count is too large");
}
const int64_t combined_code_count = combined.frames * combined.code_groups;
if (static_cast<uint64_t>(combined_code_count) > std::numeric_limits<size_t>::max()) {
throw std::runtime_error("Qwen3 speech decoder combined code count exceeds host size limits");
}
combined.codes.reserve(static_cast<size_t>(combined_code_count));
combined.codes.insert(combined.codes.end(), reference_codes.codes.begin(), reference_codes.codes.end());
combined.codes.insert(combined.codes.end(), generated_codes.codes.begin(), generated_codes.codes.end());
auto audio = decode(combined);
const int64_t cut = combined.frames > 0
? static_cast<int64_t>(
static_cast<double>(reference_codes.frames) / static_cast<double>(combined.frames) *
static_cast<double>(audio.samples.size()))
: 0;
if (cut < 0 || cut > static_cast<int64_t>(audio.samples.size())) {
if (reference_codes.frames > std::numeric_limits<int64_t>::max() / kDecodeSamplesPerCode) {
throw std::runtime_error("Qwen3 speech decoder reference sample count is too large");
}
const int64_t cut = reference_codes.frames * kDecodeSamplesPerCode;
if (static_cast<uint64_t>(cut) > audio.samples.size()) {
throw std::runtime_error("Qwen3 speech decoder reference trim is out of range");
}
audio.samples.erase(audio.samples.begin(), audio.samples.begin() + cut);
audio.samples.erase(audio.samples.begin(), audio.samples.begin() + static_cast<std::ptrdiff_t>(cut));
return audio;
}

Expand Down
Loading
Loading