Skip to content
Open
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
11 changes: 11 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ endif()
set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)

# Enable link-time optimization for optimized builds. ggml ships GGML_LTO=OFF
# by default; turning it on gives a few percent on the CPU compute kernels at
# no correctness cost. Only meaningful for optimized configs, and only if not
# already set by the user.
if(NOT DEFINED GGML_LTO AND
(CMAKE_BUILD_TYPE STREQUAL "Release" OR
CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo" OR
CMAKE_BUILD_TYPE STREQUAL "MinSizeRel"))
Comment on lines +36 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Handle multi-config generators here.

This default only works with single-config generators because CMAKE_BUILD_TYPE is empty on Visual Studio/Xcode/Ninja Multi-Config. On those generators, Release/RelWithDebInfo/MinSizeRel builds will still inherit GGML_LTO=OFF, so the new default never takes effect. Consider also checking CMAKE_CONFIGURATION_TYPES (or otherwise defaulting GGML_LTO for multi-config generators when the cache entry is unset).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CMakeLists.txt` around lines 36 - 39, The GGML_LTO default logic in
CMakeLists.txt only keys off CMAKE_BUILD_TYPE, so it is skipped for multi-config
generators like Visual Studio, Xcode, and Ninja Multi-Config where that variable
is empty. Update the GGML_LTO default handling near the existing
CMAKE_BUILD_TYPE check to also account for CMAKE_CONFIGURATION_TYPES (or another
multi-config indicator) when the cache entry is unset, so Release-like
configurations get the intended default on both single-config and multi-config
generators.

set(GGML_LTO ON CACHE BOOL "" FORCE)
endif()

if(S2_AUTO_APPLY_LOCAL_PATCHES)

if(WIN32)
Expand Down
1 change: 1 addition & 0 deletions include/s2_pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ struct PipelineParams {
std::string prompt_text;
std::string prompt_audio_path;
std::string output_path;
std::string instruction;
GenerateParams gen;
int32_t gpu_device = -1;
BackendType backend_type = BackendType::CPU;
Expand Down
3 changes: 2 additions & 1 deletion include/s2_prompt.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ PromptTensor build_prompt(
const std::string & prompt_text,
const int32_t * prompt_codes,
int32_t num_codebooks,
int32_t T_prompt
int32_t T_prompt,
const std::string & instruction = ""
);

}
2 changes: 2 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ void print_uso() {
safe_print(" -text, --text <text> Text to synthesize\n");
safe_print(" -pa, --prompt-audio <path> Path to reference audio for cloning\n");
safe_print(" -pt, --prompt-text <text> Text of the reference audio\n");
safe_print(" --instruction <text> Natural language instruction to control speaking style\n");
safe_print(" --voice <id> Load a saved voice profile\n");
safe_print(" --save-voice Save the encoded reference as a voice profile\n");
safe_print(" --voice-dir <path> Directory used for saved voice profiles\n");
Expand Down Expand Up @@ -160,6 +161,7 @@ int main(int argc, char** argv) {
else if (arg == "-text" || arg == "--text") { if (i+1 < argc) params.text = argv[++i]; }
else if (arg == "-pa" || arg == "--prompt-audio") { if (i+1 < argc) params.prompt_audio_path = argv[++i]; }
else if (arg == "-pt" || arg == "--prompt-text") { if (i+1 < argc) params.prompt_text = argv[++i]; }
else if (arg == "--instruction") { if (i+1 < argc) params.instruction = argv[++i]; }
else if (arg == "--voice") { if (i+1 < argc) params.voice_id = argv[++i]; }
else if (arg == "--save-voice") { params.save_voice = true; }
else if (arg == "--voice-dir") { if (i+1 < argc) params.voice_storage_dir = argv[++i]; }
Expand Down
10 changes: 5 additions & 5 deletions src/s2_codec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1046,7 +1046,7 @@ bool AudioCodec::encode(const float * audio, int32_t n_samples, int32_t n_thread
enc_inp.mask_values.size() * sizeof(float));
}

if (ggml_backend_is_cpu(impl_->backend)) ggml_backend_cpu_set_n_threads(impl_->backend, n_threads);
if (ggml_backend_is_cpu(impl_->backend)) ggml_backend_cpu_set_n_threads(impl_->backend, resolve_n_threads(n_threads));
if (ggml_backend_graph_compute(impl_->backend, gf) != GGML_STATUS_SUCCESS) {
std::cerr << "[Codec::encode] encoder compute failed." << std::endl;
ggml_gallocr_free(allocr);
Expand Down Expand Up @@ -1119,7 +1119,7 @@ bool AudioCodec::encode(const float * audio, int32_t n_samples, int32_t n_thread
qenc_inp.mask_values.size() * sizeof(float));
}

if (ggml_backend_is_cpu(impl_->backend)) ggml_backend_cpu_set_n_threads(impl_->backend, n_threads);
if (ggml_backend_is_cpu(impl_->backend)) ggml_backend_cpu_set_n_threads(impl_->backend, resolve_n_threads(n_threads));
if (ggml_backend_graph_compute(impl_->backend, gf2) != GGML_STATUS_SUCCESS) {
std::cerr << "[Codec::encode] quantizer stage compute failed." << std::endl;
ggml_gallocr_free(allocr2);
Expand Down Expand Up @@ -1279,7 +1279,7 @@ static bool run_cached_decode_graph(AudioCodec::Impl & impl, const int32_t * cod
}

if (ggml_backend_is_cpu(impl.backend)) {
ggml_backend_cpu_set_n_threads(impl.backend, n_threads);
ggml_backend_cpu_set_n_threads(impl.backend, resolve_n_threads(n_threads));
}

if (ggml_backend_graph_compute(impl.backend, cache.graph) != GGML_STATUS_SUCCESS) {
Expand Down Expand Up @@ -1381,7 +1381,7 @@ bool AudioCodec::decode(const int32_t * codes, int32_t n_frames, int32_t n_threa
inp.mask_values.size() * sizeof(float));
}

if (ggml_backend_is_cpu(impl_->backend)) ggml_backend_cpu_set_n_threads(impl_->backend, n_threads);
if (ggml_backend_is_cpu(impl_->backend)) ggml_backend_cpu_set_n_threads(impl_->backend, resolve_n_threads(n_threads));

if (ggml_backend_graph_compute(impl_->backend, gf) != GGML_STATUS_SUCCESS) {
std::cerr << "[Codec::decode] quantizer decode compute failed." << std::endl;
Expand Down Expand Up @@ -1426,7 +1426,7 @@ bool AudioCodec::decode(const int32_t * codes, int32_t n_frames, int32_t n_threa

ggml_backend_tensor_set(latent_in, latent_out.data(), 0, latent_out.size() * sizeof(float));

if (ggml_backend_is_cpu(impl_->backend)) ggml_backend_cpu_set_n_threads(impl_->backend, n_threads);
if (ggml_backend_is_cpu(impl_->backend)) ggml_backend_cpu_set_n_threads(impl_->backend, resolve_n_threads(n_threads));

if (ggml_backend_graph_compute(impl_->backend, gf) != GGML_STATUS_SUCCESS) {
std::cerr << "[Codec::decode] decoder compute failed." << std::endl;
Expand Down
88 changes: 88 additions & 0 deletions src/s2_ggml_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,97 @@
#include "ggml.h"

#include <cstdio>
#include <cstdint>
#include <thread>

#if defined(__APPLE__)
# include <sys/sysctl.h>
#elif defined(_WIN32)
# ifndef WIN32_LEAN_AND_MEAN
# define WIN32_LEAN_AND_MEAN
# endif
# include <windows.h>
# include <vector>
#elif defined(__linux__)
# include <cstdio>
# include <set>
# include <utility>
#endif

namespace s2 {

// Number of physical "performance" cores available for compute.
//
// ggml's CPU backend scales poorly when oversubscribed onto hyperthreads or,
// on Apple Silicon, onto the slow efficiency cores: every thread in a fork/join
// matmul waits on the slowest one, so the whole step runs at efficiency-core
// speed. Measured on an M4 Pro (10P + 4E), defaulting to all 14 logical cores
// was ~2.9x slower end-to-end than capping at the 10 performance cores.
//
// This returns a sensible default core count; callers fall back to it only when
// the user did not pass an explicit --threads value.
inline int32_t detect_compute_threads() {
#if defined(__APPLE__)
// Performance-core count (perflevel0). Falls back to all physical cores.
int32_t n = 0;
size_t sz = sizeof(n);
if (sysctlbyname("hw.perflevel0.physicalcpu", &n, &sz, nullptr, 0) == 0 && n > 0) {
return n;
}
n = 0; sz = sizeof(n);
if (sysctlbyname("hw.physicalcpu", &n, &sz, nullptr, 0) == 0 && n > 0) {
return n;
}
#elif defined(_WIN32)
// Count physical cores via processor-core relationships.
DWORD len = 0;
GetLogicalProcessorInformation(nullptr, &len);
if (len > 0) {
std::vector<SYSTEM_LOGICAL_PROCESSOR_INFORMATION> buf(
len / sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION));
if (GetLogicalProcessorInformation(buf.data(), &len)) {
int32_t cores = 0;
for (const auto & info : buf) {
if (info.Relationship == RelationProcessorCore) cores++;
}
if (cores > 0) return cores;
}
}
Comment on lines +47 to +61

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Does GetLogicalProcessorInformation report processors across all processor groups on systems with more than 64 logical processors?

💡 Result:

No, the GetLogicalProcessorInformation function does not report processors across all processor groups on systems with more than 64 logical processors [1][2]. On such systems, GetLogicalProcessorInformation only retrieves information about the processors within the specific processor group to which the calling thread is currently assigned [1][2][3]. To retrieve information about logical processors across all processor groups in the system, you must use the GetLogicalProcessorInformationEx function instead [1][2][4]. GetLogicalProcessorInformationEx is designed to handle systems with more than 64 processors by providing information for every active processor group [5].

Citations:


🏁 Script executed:

# Inspect the relevant code and nearby fallback logic in src/s2_ggml_utils.h
sed -n '1,140p' src/s2_ggml_utils.h

Repository: rodrigomatta/s2.cpp

Length of output: 4861


🏁 Script executed:

sed -n '1,140p' src/s2_ggml_utils.h

Repository: rodrigomatta/s2.cpp

Length of output: 4861


Use GetLogicalProcessorInformationEx for high-core Windows hosts. GetLogicalProcessorInformation only reports the current processor group, so this branch can return a truncated nonzero core count on systems with more than 64 logical processors; the hardware_concurrency() fallback only runs when the count is zero.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/s2_ggml_utils.h` around lines 47 - 61, The Windows core-count path in
s2_ggml_utils.h currently uses GetLogicalProcessorInformation, which can
undercount on machines with more than 64 logical processors. Update the Windows
branch in the hardware core detection helper to use
GetLogicalProcessorInformationEx and iterate all processor groups when counting
physical cores, falling back to hardware_concurrency only if no valid count is
found. Keep the change localized to the core-count logic in this function so
high-core hosts return an accurate non-truncated value.

#elif defined(__linux__)
// Count distinct (physical_id, core_id) pairs from /proc/cpuinfo to skip
// SMT siblings.
if (FILE * f = std::fopen("/proc/cpuinfo", "r")) {
std::set<std::pair<int, int>> cores;
int phys = -1, core = -1;
char line[256];
while (std::fgets(line, sizeof(line), f)) {
int v;
if (std::sscanf(line, "physical id : %d", &v) == 1) phys = v;
else if (std::sscanf(line, "core id : %d", &v) == 1) core = v;
if (line[0] == '\n') { // blank line separates logical CPUs
if (phys >= 0 && core >= 0) cores.insert({phys, core});
phys = core = -1;
}
}
if (phys >= 0 && core >= 0) cores.insert({phys, core});
std::fclose(f);
if (!cores.empty()) return static_cast<int32_t>(cores.size());
}
#endif
// Generic fallback: assume SMT and halve the logical-core count, but never
// go below 1.
const unsigned hw = std::thread::hardware_concurrency();
if (hw == 0) return 4;
return static_cast<int32_t>(hw >= 2 ? hw / 2 : hw);
}

// Resolve a caller-supplied thread count: honor explicit positive values,
// otherwise pick a good default for this machine.
inline int32_t resolve_n_threads(int32_t n_threads) {
if (n_threads > 0) return n_threads;
return detect_compute_threads();
}

inline ggml_tensor * repeat_checked(ggml_context * ctx, ggml_tensor * a, ggml_tensor * b,
const char * label = "repeat") {
if (!ggml_can_repeat(a, b)) {
Expand Down
6 changes: 0 additions & 6 deletions src/s2_model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@

namespace s2 {

static int32_t resolve_n_threads(int32_t n_threads) {
if (n_threads > 0) return n_threads;
const auto hw = std::thread::hardware_concurrency();
return static_cast<int32_t>(hw > 0 ? hw : 4);
}

static const char * backend_type_name(BackendType backend_type) {
switch (backend_type) {
case BackendType::CPU: return "CPU";
Expand Down
17 changes: 13 additions & 4 deletions src/s2_pipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -844,7 +844,7 @@ bool Pipeline::synthesize_prompt_codes_locked(const PipelineParams & params, con
PromptTensor prompt = build_prompt(
tokenizer(), params.text, params.prompt_text,
ref_codes,
num_codebooks, T_prompt);
num_codebooks, T_prompt, params.instruction);

int32_t max_seq_len = prompt.cols + params.gen.max_new_tokens;
const auto kv_t0 = std::chrono::steady_clock::now();
Expand All @@ -863,14 +863,23 @@ bool Pipeline::synthesize_prompt_codes_locked(const PipelineParams & params, con
return false;
}

// Offline synthesis has every frame in hand, so there is no latency reason
// to chunk the codec decode. Decoding all frames in a single pass is both
// faster (one graph build / allocation instead of one per window, and no
// re-decoding of the overlapping history region) and cleaner (no window
// boundary artifacts). Streaming uses the windowed path; offline defaults to
// a single shot unless the user explicitly sets a decode stride.
const bool user_set_stride = params.stream_decode_stride_frames > 0;
const int32_t offline_decode_stride_frames =
params.stream_decode_stride_frames > 0 ? params.stream_decode_stride_frames : 16;
user_set_stride ? params.stream_decode_stride_frames : res.n_frames;
const int32_t offline_decode_context_frames =
user_set_stride ? params.codec_decode_context_frames : 0;
double decode_ms = 0.0;
int32_t decode_batches = 0;
const auto decode_t0 = std::chrono::steady_clock::now();
if (!decode_codes_windowed(codec(), res.codes.data(), res.n_frames, num_codebooks,
params.gen.n_threads, offline_decode_stride_frames,
params.codec_decode_context_frames,
offline_decode_context_frames,
audio_out, &decode_ms, &decode_batches)) {
safe_print_error_ln("Pipeline error: decode failed.");
return false;
Expand Down Expand Up @@ -982,7 +991,7 @@ bool Pipeline::synthesize_streaming_prompt_codes_locked(const PipelineParams & p
PromptTensor prompt = build_prompt(
tokenizer(), params.text, params.prompt_text,
ref_codes,
num_codebooks, T_prompt);
num_codebooks, T_prompt, params.instruction);

int32_t max_seq_len = prompt.cols + params.gen.max_new_tokens;
const auto kv_t0 = std::chrono::steady_clock::now();
Expand Down
15 changes: 12 additions & 3 deletions src/s2_prompt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ PromptTensor build_prompt(
const std::string & prompt_text,
const int32_t * prompt_codes,
int32_t num_codebooks,
int32_t T_prompt
int32_t T_prompt,
const std::string & instruction
) {
PromptTensor result;
result.rows = num_codebooks + 1;
Expand All @@ -33,7 +34,11 @@ PromptTensor build_prompt(

app(sys_pre, tokenizer.encode("<|im_start|>system"));
app(sys_pre, NEWLINE);
app(sys_pre, tokenizer.encode("convert the provided text to speech reference to the following:\n\nText:\n"));
std::string sys_msg = "convert the provided text to speech reference to the following:\n\nText:\n";
if (!instruction.empty()) {
sys_msg = instruction + "\n\n" + sys_msg;
}
app(sys_pre, tokenizer.encode(sys_msg));
if (!prompt_has_speaker_tag) {
app(sys_pre, tokenizer.encode("<|speaker:0|>"));
}
Expand All @@ -57,7 +62,11 @@ PromptTensor build_prompt(

app(sys_post, tokenizer.encode("<|im_start|>system"));
app(sys_post, NEWLINE);
app(sys_post, tokenizer.encode("convert the provided text to speech"));
std::string sys_msg = "convert the provided text to speech";
if (!instruction.empty()) {
sys_msg += ". " + instruction;
}
app(sys_post, tokenizer.encode(sys_msg));
app(sys_post, { im_end_id });
app(sys_post, NEWLINE);

Expand Down
77 changes: 43 additions & 34 deletions src/s2_sampler.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "../include/s2_sampler.h"
#include <cmath>
#include <algorithm>
#include <limits>
#include <random>

namespace s2 {
Expand All @@ -22,50 +23,58 @@ static void apply_softmax(std::vector<float> & probs, float temp = 1.0f) {
}
}

static std::vector<float> softmax_from_sorted_logits(const std::vector<std::pair<float, int32_t>> & items) {
std::vector<float> probs(items.size(), 0.0f);
if (items.empty()) return probs;

const float max_val = items.front().first;
float sum = 0.0f;
for (size_t i = 0; i < items.size(); ++i) {
probs[i] = std::exp(items[i].first - max_val);
sum += probs[i];
}

if (sum > 0.0f) {
for (float & p : probs) p /= sum;
}
return probs;
}

int32_t sample_token(const float * logits, int32_t vocab_size, const SamplerParams & params) {
if (vocab_size <= 0) return 0;

const int32_t k = params.top_k > 0 ? std::min(params.top_k, vocab_size) : vocab_size;
const float top_p = std::clamp(params.top_p, 0.0f, 1.0f);
const float NEG_INF = -std::numeric_limits<float>::infinity();

// Single pass: gather finite candidates and the global max. Masked logits
// (set to -inf by the caller, e.g. the semantic mask covering ~95% of the
// vocab) contribute exp(-inf)=0 to the softmax and can never be selected,
// so skipping them is exact and avoids building/sorting the full vocab.
std::vector<std::pair<float, int32_t>> items;
items.reserve(vocab_size);
items.reserve(static_cast<size_t>(std::min(vocab_size, 8192)));
float max_val = NEG_INF;
for (int32_t i = 0; i < vocab_size; ++i) {
items.push_back({logits[i], i});
const float v = logits[i];
if (v == NEG_INF) continue;
items.push_back({v, i});
if (v > max_val) max_val = v;
}
Comment on lines 40 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check includes and NEG_INF definition in src/s2_sampler.cpp
cat -n src/s2_sampler.cpp | head -100

Repository: rodrigomatta/s2.cpp

Length of output: 4539


🏁 Script executed:

python3 - <<'PY'
import math

# Verify isfinite behavior for edge cases
test_values = [
    (float('-inf'), False, "NEG_INF"),
    (float('inf'), False, "+inf"),
    (float('nan'), False, "NaN"),
    (0.0, True, "zero"),
    (-10.0, True, "negative"),
    (10.0, True, "positive"),
]

print("Testing isfinite behavior:")
for val, expected_finite, desc in test_values:
    is_finite = math.isfinite(val)
    status = "✓" if is_finite == expected_finite else "✗"
    print(f"{status} {desc}: value={val}, is_finite={is_finite}, expected={expected_finite}")

print("\nAll edge cases handled correctly by isfinite:")
print("- NaN: filtered out (not finite)")
print("- +inf: filtered out (not finite)")
print("- -inf: filtered out (not finite)")
print("- Finite values: kept (finite)")
PY

Repository: rodrigomatta/s2.cpp

Length of output: 677


Filter all non-finite logits, not only exact -inf.

The current check if (v == NEG_INF) only excludes negative infinity. NaN and +inf logits still enter items, causing std::exp in subsequent probability calculations to produce invalid results (NaN or overflow). Since this path filters for finite candidates, replace the equality check with std::isfinite(v).

Proposed fix
     for (int32_t i = 0; i < vocab_size; ++i) {
         const float v = logits[i];
-        if (v == NEG_INF) continue;
+        if (!std::isfinite(v)) continue;
         items.push_back({v, i});
         if (v > max_val) max_val = v;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (int32_t i = 0; i < vocab_size; ++i) {
items.push_back({logits[i], i});
const float v = logits[i];
if (v == NEG_INF) continue;
items.push_back({v, i});
if (v > max_val) max_val = v;
}
for (int32_t i = 0; i < vocab_size; ++i) {
const float v = logits[i];
if (!std::isfinite(v)) continue;
items.push_back({v, i});
if (v > max_val) max_val = v;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/s2_sampler.cpp` around lines 40 - 45, The candidate collection in the
logits loop only excludes exact NEG_INF, so non-finite values like NaN and +inf
can still flow into the S2 sampling path and break later probability math.
Update the filtering in the loop that builds items in s2_sampler.cpp to reject
any non-finite value instead of comparing only against NEG_INF, and keep the
max_val update tied to the same finite-candidate check.

if (items.empty()) return 0;

// Full partition function over the finite candidates. This matches the
// original code's denominator (the -inf terms it summed were exactly 0),
// so the top_p cumulative mass below is computed identically.
float Z = 0.0f;
for (const auto & it : items) Z += std::exp(it.first - max_val);
if (!(Z > 0.0f)) Z = 1.0f;

// Order by logit descending, breaking ties by index for determinism.
auto cmp = [](const std::pair<float, int32_t> & a, const std::pair<float, int32_t> & b) {
if (a.first != b.first) return a.first > b.first;
return a.second < b.second;
};
const int32_t kk = std::min<int32_t>(k, static_cast<int32_t>(items.size()));
if (kk < static_cast<int32_t>(items.size())) {
// top_k is small (default 30); only the top kk elements are ever kept,
// so a partial sort (O(n log kk)) replaces a full sort (O(n log n)).
std::partial_sort(items.begin(), items.begin() + kk, items.end(), cmp);
items.resize(kk);
} else {
std::sort(items.begin(), items.end(), cmp);
}

std::sort(items.begin(), items.end(), [](const auto & a, const auto & b) {
return a.first > b.first;
});

const int32_t k = params.top_k > 0 ? std::min(params.top_k, vocab_size) : vocab_size;
const float top_p = std::clamp(params.top_p, 0.0f, 1.0f);
const std::vector<float> sorted_probs = softmax_from_sorted_logits(items);

// Keep the prefix up to top_k and the top_p cumulative-probability cutoff.
std::vector<std::pair<float, int32_t>> filtered;
filtered.reserve(k);

filtered.reserve(items.size());
float cumsum = 0.0f;
for (int32_t i = 0; i < (int32_t)items.size(); ++i) {
cumsum += sorted_probs[i];
const bool remove_for_top_k = (i >= k);
const bool remove_for_top_p = (i > 0 && cumsum > top_p);
if (remove_for_top_k || remove_for_top_p) {
continue;
cumsum += std::exp(items[i].first - max_val) / Z;
if (i > 0 && cumsum > top_p) {
break; // cumsum is monotonic, so every later item also exceeds top_p
}
filtered.push_back(items[i]);
Comment on lines 73 to 79

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Include the token that crosses the top_p threshold.

The current loop breaks before adding the crossing token, so top_p=0.8 can retain only 60% mass when the next token would bring the prefix to 85%. Push first, then stop once the retained prefix reaches the cutoff.

Proposed fix
     float cumsum = 0.0f;
     for (int32_t i = 0; i < (int32_t)items.size(); ++i) {
         cumsum += std::exp(items[i].first - max_val) / Z;
-        if (i > 0 && cumsum > top_p) {
-            break; // cumsum is monotonic, so every later item also exceeds top_p
-        }
         filtered.push_back(items[i]);
+        if (top_p < 1.0f && cumsum >= top_p) {
+            break; // cumsum is monotonic, so every later item also exceeds top_p
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
float cumsum = 0.0f;
for (int32_t i = 0; i < (int32_t)items.size(); ++i) {
cumsum += sorted_probs[i];
const bool remove_for_top_k = (i >= k);
const bool remove_for_top_p = (i > 0 && cumsum > top_p);
if (remove_for_top_k || remove_for_top_p) {
continue;
cumsum += std::exp(items[i].first - max_val) / Z;
if (i > 0 && cumsum > top_p) {
break; // cumsum is monotonic, so every later item also exceeds top_p
}
filtered.push_back(items[i]);
float cumsum = 0.0f;
for (int32_t i = 0; i < (int32_t)items.size(); ++i) {
cumsum += std::exp(items[i].first - max_val) / Z;
filtered.push_back(items[i]);
if (top_p < 1.0f && cumsum >= top_p) {
break; // cumsum is monotonic, so every later item also exceeds top_p
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/s2_sampler.cpp` around lines 73 - 79, The top-p filtering loop in
s2_sampler.cpp is dropping the token that first pushes the cumulative
probability over the cutoff, so the retained prefix can end too early. Update
the loop in the sampling logic around the cumsum/top_p check so the current item
is added to filtered before evaluating whether to stop, and keep the break
condition tied to the retained prefix reaching the threshold in the relevant
sampler function.

}
Expand Down
4 changes: 4 additions & 0 deletions src/s2_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,10 @@ namespace s2
voice_dir_value = j["voice_dir"].get<std::string>();
}

if (j.contains("instruction")) {
pipelineParams.instruction = j["instruction"].get<std::string>();
}

if (j.contains("stream_decode_stride_frames")) {
int32_t val = j["stream_decode_stride_frames"].get<int32_t>();
pipelineParams.stream_decode_stride_frames = std::max(0, val);
Expand Down