Skip to content

Commit 2afccae

Browse files
authored
Improve Chatterbox memory usage
Improve Chatterbox memory usage
2 parents 92fd23a + 4e613b9 commit 2afccae

20 files changed

Lines changed: 434 additions & 65 deletions

File tree

app/server/main.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
#include "http.h"
33
#include "runtime.h"
44

5+
#include "engine/framework/debug/trace.h"
6+
57
#include <filesystem>
68
#include <iostream>
79
#include <optional>
@@ -31,6 +33,7 @@ bool has_arg(int argc, char ** argv, const std::string & name) {
3133
void print_help() {
3234
std::cout
3335
<< "audiocpp_server --config <server.json> [--host <ip>] [--port <port>] [--device <id>] [--threads <n>]\n"
36+
<< " [--log] [--log-file <path>]\n"
3437
<< "\n"
3538
<< "Endpoints:\n"
3639
<< " GET /health\n"
@@ -52,6 +55,11 @@ int main(int argc, char ** argv) {
5255
if (!config_path.has_value()) {
5356
throw std::runtime_error("missing required --config argument");
5457
}
58+
const auto log_file = arg_value(argc, argv, "--log-file");
59+
engine::debug::configure_logging(engine::debug::LoggingConfig{
60+
has_arg(argc, argv, "--log") || log_file.has_value(),
61+
log_file,
62+
});
5563

5664
auto config = minitts::server::load_server_config(*config_path);
5765
if (const auto host = arg_value(argc, argv, "--host")) {

app/server/runtime.cpp

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,21 @@
66
#include "engine/framework/runtime/registry.h"
77

88
#include <algorithm>
9+
#include <chrono>
910
#include <cmath>
1011
#include <cstdint>
1112
#include <sstream>
1213
#include <stdexcept>
14+
#include <unordered_map>
1315
#include <utility>
1416

1517
namespace minitts::server {
1618
namespace {
1719

1820
using engine::io::json::Value;
1921

22+
using Clock = std::chrono::steady_clock;
23+
2024
std::string json_quote(std::string_view value) {
2125
return engine::io::json::stringify_string(value);
2226
}
@@ -108,7 +112,49 @@ std::string base64_encode(const std::vector<uint8_t> & bytes) {
108112
return base64_encode(bytes.data(), bytes.size());
109113
}
110114

111-
std::string task_result_json(const engine::runtime::TaskResult & result) {
115+
double elapsed_ms(Clock::time_point started) {
116+
return std::chrono::duration<double, std::milli>(Clock::now() - started).count();
117+
}
118+
119+
double audio_duration_ms(const engine::runtime::AudioBuffer & audio) {
120+
if (audio.sample_rate <= 0 || audio.channels <= 0) {
121+
return 0.0;
122+
}
123+
return 1000.0 * static_cast<double>(audio.samples.size()) /
124+
static_cast<double>(audio.sample_rate * audio.channels);
125+
}
126+
127+
double audio_rtf(double wall_ms, double duration_ms) {
128+
return duration_ms > 0.0 ? wall_ms / duration_ms : 0.0;
129+
}
130+
131+
std::string timing_json(double wall_ms) {
132+
std::ostringstream out;
133+
out << "{\"wall_ms\":" << wall_ms << "}";
134+
return out.str();
135+
}
136+
137+
std::string timing_json(double wall_ms, const engine::runtime::AudioBuffer & audio) {
138+
const double duration_ms = audio_duration_ms(audio);
139+
std::ostringstream out;
140+
out << "{\"wall_ms\":" << wall_ms
141+
<< ",\"audio_duration_ms\":" << duration_ms
142+
<< ",\"rtf\":" << audio_rtf(wall_ms, duration_ms) << "}";
143+
return out.str();
144+
}
145+
146+
std::unordered_map<std::string, std::string> timing_headers(
147+
double wall_ms,
148+
const engine::runtime::AudioBuffer & audio) {
149+
const double duration_ms = audio_duration_ms(audio);
150+
return {
151+
{"X-AudioCPP-Wall-Ms", std::to_string(wall_ms)},
152+
{"X-AudioCPP-Audio-Duration-Ms", std::to_string(duration_ms)},
153+
{"X-AudioCPP-RTF", std::to_string(audio_rtf(wall_ms, duration_ms))},
154+
};
155+
}
156+
157+
std::string task_result_json(const engine::runtime::TaskResult & result, double wall_ms) {
112158
std::ostringstream out;
113159
out << "{";
114160
bool first = true;
@@ -197,6 +243,14 @@ std::string task_result_json(const engine::runtime::TaskResult & result) {
197243
}
198244
out << "]";
199245
}
246+
field("timing");
247+
if (result.audio_output.has_value()) {
248+
out << timing_json(wall_ms, *result.audio_output);
249+
} else if (result.named_audio_outputs.size() == 1) {
250+
out << timing_json(wall_ms, result.named_audio_outputs.front().audio);
251+
} else {
252+
out << timing_json(wall_ms);
253+
}
200254
out << "}";
201255
return out.str();
202256
}
@@ -364,37 +418,55 @@ ServerState::LoadedModel & ServerState::require_model(const Value & body) {
364418
return *models_.at(it->second);
365419
}
366420

367-
engine::runtime::TaskResult ServerState::run_model(
421+
struct ServerState::TimedTaskResult {
422+
engine::runtime::TaskResult result;
423+
double wall_ms = 0.0;
424+
};
425+
426+
ServerState::TimedTaskResult ServerState::run_model(
368427
LoadedModel & model,
369428
const engine::runtime::TaskRequest & request) {
370429
std::lock_guard<std::mutex> lock(model.mutex);
371430
ensure_model_loaded_locked(model);
431+
const auto started = Clock::now();
372432
model.session->prepare(engine::runtime::build_preparation_request(request));
373-
return model.offline->run(request);
433+
auto result = model.offline->run(request);
434+
return TimedTaskResult{std::move(result), elapsed_ms(started)};
374435
}
375436

376437
HttpResponse ServerState::handle_speech(const std::string & body_text) {
377438
const auto body = engine::io::json::parse(body_text);
378439
auto & model = require_model(body);
379440
const auto request = build_openai_speech_request(body, request_base_);
380-
const auto result = run_model(model, request);
381-
const auto wav = encode_pcm16_wav(select_audio_output(result));
441+
const auto timed_result = run_model(model, request);
442+
const auto & audio = select_audio_output(timed_result.result);
443+
const auto wav = encode_pcm16_wav(audio);
382444
const auto response_format = engine::io::json::optional_string(body, "response_format", "wav");
383445
if (response_format == "json" || response_format == "b64_json") {
384-
return json_response("{\"audio\":" + json_quote(base64_encode(wav)) + ",\"format\":\"wav\"}");
385-
}
386-
return HttpResponse{200, "audio/wav", std::string(reinterpret_cast<const char *>(wav.data()), wav.size()), {}};
446+
return json_response(
447+
"{\"audio\":" + json_quote(base64_encode(wav)) +
448+
",\"format\":\"wav\",\"timing\":" + timing_json(timed_result.wall_ms, audio) + "}");
449+
}
450+
return HttpResponse{
451+
200,
452+
"audio/wav",
453+
std::string(reinterpret_cast<const char *>(wav.data()), wav.size()),
454+
timing_headers(timed_result.wall_ms, audio),
455+
};
387456
}
388457

389458
HttpResponse ServerState::handle_transcription(const std::string & body_text) {
390459
const auto body = engine::io::json::parse(body_text);
391460
auto & model = require_model(body);
392461
const auto request = build_openai_transcription_request(body, request_base_);
393-
const auto result = run_model(model, request);
462+
const auto timed_result = run_model(model, request);
463+
const auto & result = timed_result.result;
394464
if (!result.text_output.has_value()) {
395465
throw std::runtime_error("model result did not contain transcript text");
396466
}
397-
return json_response("{\"text\":" + json_quote(result.text_output->text) + "}");
467+
return json_response(
468+
"{\"text\":" + json_quote(result.text_output->text) +
469+
",\"timing\":" + timing_json(timed_result.wall_ms) + "}");
398470
}
399471

400472
HttpResponse ServerState::handle_generic_run(const std::string & body_text) {
@@ -404,7 +476,8 @@ HttpResponse ServerState::handle_generic_run(const std::string & body_text) {
404476
const auto request = minitts::cli::build_request_from_json(
405477
request_json != nullptr ? *request_json : body,
406478
request_base_);
407-
return json_response(task_result_json(run_model(model, request)));
479+
const auto timed_result = run_model(model, request);
480+
return json_response(task_result_json(timed_result.result, timed_result.wall_ms));
408481
}
409482

410483
std::string ServerState::models_json() const {

app/server/runtime.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ class ServerState final : public IHttpHandler {
3434
void load_models();
3535
void ensure_model_loaded_locked(LoadedModel & model);
3636
LoadedModel & require_model(const engine::io::json::Value & body);
37-
engine::runtime::TaskResult run_model(LoadedModel & model, const engine::runtime::TaskRequest & request);
37+
struct TimedTaskResult;
38+
TimedTaskResult run_model(LoadedModel & model, const engine::runtime::TaskRequest & request);
3839
HttpResponse handle_speech(const std::string & body_text);
3940
HttpResponse handle_transcription(const std::string & body_text);
4041
HttpResponse handle_generic_run(const std::string & body_text);

docs/tts.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ audiocpp_cli --task clon --family chatterbox --model models/chatterbox --backend
4242
|---|---|---:|---|
4343
| `--voice-ref` | WAV path | required | Reference speaker audio. |
4444
| `--language` | language code | `en` | Text language. |
45-
| `--text-chunk-size` | integer chars | `256` | Long-form chunk size. |
45+
| `--text-chunk-size` | integer chars | `128` | Long-form chunk size. |
4646
| `--guidance-scale` | float | `0.5` | CFG strength. |
4747
| `--temperature` | float | `0.8` | T3 sampling temperature. |
4848
| `--top-p` | float | `0.8` | T3 nucleus sampling limit. |

include/engine/framework/modules/hift_vocoder.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ class HiftVocoderComponent {
144144
uint64_t prior_noise_values = 0,
145145
const std::vector<float> * source_random_values = nullptr) const;
146146
std::vector<float> predict_f0(const std::vector<float> & mel, int64_t frames) const;
147+
void release_runtime_cache() const;
147148

148149
private:
149150
struct State;
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#pragma once
2+
3+
#include <cstddef>
4+
#include <cstdint>
5+
#include <functional>
6+
#include <utility>
7+
#include <vector>
8+
9+
namespace engine::runtime {
10+
11+
template <typename Key, typename Value, typename Equal = std::equal_to<Key>>
12+
class CacheSlots {
13+
public:
14+
explicit CacheSlots(std::size_t capacity = 1)
15+
: capacity_(capacity) {}
16+
17+
std::size_t capacity() const noexcept {
18+
return capacity_;
19+
}
20+
21+
std::size_t size() const noexcept {
22+
return entries_.size();
23+
}
24+
25+
void set_capacity(std::size_t capacity) {
26+
capacity_ = capacity;
27+
evict_to_capacity();
28+
}
29+
30+
Value * find(const Key & key) {
31+
for (auto & entry : entries_) {
32+
if (equal_(entry.key, key)) {
33+
entry.last_used = next_tick();
34+
return &entry.value;
35+
}
36+
}
37+
return nullptr;
38+
}
39+
40+
const Value * find(const Key & key) const {
41+
for (const auto & entry : entries_) {
42+
if (equal_(entry.key, key)) {
43+
return &entry.value;
44+
}
45+
}
46+
return nullptr;
47+
}
48+
49+
void put(Key key, Value value) {
50+
if (capacity_ == 0) {
51+
entries_.clear();
52+
return;
53+
}
54+
for (auto & entry : entries_) {
55+
if (equal_(entry.key, key)) {
56+
entry.key = std::move(key);
57+
entry.value = std::move(value);
58+
entry.last_used = next_tick();
59+
return;
60+
}
61+
}
62+
if (entries_.size() >= capacity_) {
63+
erase_lru();
64+
}
65+
entries_.push_back(Entry{
66+
std::move(key),
67+
std::move(value),
68+
next_tick(),
69+
});
70+
}
71+
72+
void clear() {
73+
entries_.clear();
74+
}
75+
76+
private:
77+
struct Entry {
78+
Key key;
79+
Value value;
80+
std::uint64_t last_used = 0;
81+
};
82+
83+
std::uint64_t next_tick() noexcept {
84+
return ++tick_;
85+
}
86+
87+
void evict_to_capacity() {
88+
while (entries_.size() > capacity_) {
89+
erase_lru();
90+
}
91+
}
92+
93+
void erase_lru() {
94+
if (entries_.empty()) {
95+
return;
96+
}
97+
std::size_t oldest = 0;
98+
for (std::size_t i = 1; i < entries_.size(); ++i) {
99+
if (entries_[i].last_used < entries_[oldest].last_used) {
100+
oldest = i;
101+
}
102+
}
103+
entries_.erase(entries_.begin() + static_cast<std::ptrdiff_t>(oldest));
104+
}
105+
106+
std::size_t capacity_ = 1;
107+
std::uint64_t tick_ = 0;
108+
std::vector<Entry> entries_;
109+
Equal equal_;
110+
};
111+
112+
} // namespace engine::runtime

include/engine/models/chatterbox/components.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ class HiFTVocoderComponent {
199199
uint64_t seed,
200200
uint64_t prior_noise_values,
201201
const std::vector<float> & cache_source) const;
202+
void release_runtime_cache() const;
202203

203204
private:
204205
struct State;

include/engine/models/chatterbox/s3gen_inference.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class S3GenSessionCache {
2222
S3GenSessionCache(const S3GenSessionCache &) = delete;
2323
S3GenSessionCache & operator=(const S3GenSessionCache &) = delete;
2424

25+
void release_runtime_graphs();
26+
2527
private:
2628
struct State;
2729
std::unique_ptr<State> state_;

include/engine/models/chatterbox/session.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,31 @@
22

33
#include "engine/framework/runtime/session_base.h"
44
#include "engine/framework/assets/tensor_source.h"
5+
#include "engine/framework/runtime/cache_slots.h"
56
#include "engine/models/chatterbox/assets.h"
67
#include "engine/models/chatterbox/conditionals.h"
78
#include "engine/models/chatterbox/tts.h"
89

10+
#include <cstddef>
911
#include <filesystem>
1012
#include <memory>
1113
#include <optional>
14+
#include <string>
1215

1316
namespace engine::models::chatterbox {
1417

18+
struct ChatterboxConditionalsCacheKey {
19+
runtime::AudioBuffer reference_audio;
20+
float exaggeration = 0.0f;
21+
std::string language;
22+
};
23+
24+
struct ChatterboxConditionalsCacheKeyEqual {
25+
bool operator()(
26+
const ChatterboxConditionalsCacheKey & lhs,
27+
const ChatterboxConditionalsCacheKey & rhs) const;
28+
};
29+
1530
class ChatterboxSession final
1631
: public runtime::RuntimeSessionBase
1732
, public runtime::IOfflineVoiceTaskSession {
@@ -35,8 +50,14 @@ class ChatterboxSession final
3550
std::shared_ptr<const ChatterboxAssetPaths> assets_;
3651
engine::assets::TensorStorageType t3_weight_storage_type_ = engine::assets::TensorStorageType::Native;
3752
engine::assets::TensorStorageType component_weight_storage_type_ = engine::assets::TensorStorageType::Native;
53+
bool mem_saver_ = false;
3854
std::unique_ptr<ChatterboxTtsComponent> component_;
55+
std::optional<std::string> component_language_;
3956
std::optional<ChatterboxVoiceCloneConfig> voice_clone_config_;
57+
runtime::CacheSlots<
58+
ChatterboxConditionalsCacheKey,
59+
ChatterboxConditionalsOutputs,
60+
ChatterboxConditionalsCacheKeyEqual> conditionals_cache_;
4061
std::optional<ChatterboxConditionalsOutputs> cached_conditionals_;
4162
double cached_prompt_prep_ms_ = 0.0;
4263
};

0 commit comments

Comments
 (0)