Skip to content

Add dense score retrieval for certain ScoreAccessors#223

Open
SimBe195 wants to merge 4 commits into
multi_scorer_labelsyncfrom
dense_score_accessors
Open

Add dense score retrieval for certain ScoreAccessors#223
SimBe195 wants to merge 4 commits into
multi_scorer_labelsyncfrom
dense_score_accessors

Conversation

@SimBe195
Copy link
Copy Markdown
Collaborator

@SimBe195 SimBe195 commented May 15, 2026

Some ScoreAccessors (e.g. VectorScoreAccessor) represent a dense span of score values over the vocabulary that don't depend on the transition type. For these ScoreAccessor classes we can implement an optimized function to retrieve the full dense score span directly and thus fetch all scores at once. This avoids repeated virtual scoreAccessor->getScore(transitionType, labelIndex) function calls in the search algorithms for every possible extension. In the future it can also be used to introduce local pruning over the dense score span when it's available.

Originally I modeled the DenseScoreSpan just as a std::span<Score const> but that doesn't support scaling and interpolation as it is used in CombineLabelScorer, PriorLabelScorer and ScaledLabelScorer. So now it is instead modeled as a struct containing multiple terms each of which consist of a span + a scale.

In a test segment with a speech LLM and lexiconfree labelsync search, the dense score accessors reduce the search time from 13.39s to 12.85s (RTF 0.336 to 0.323).

Comment on lines +44 to +46
size_t size() const {
return terms.empty() ? 0ul : terms.front().scores.size();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this might be confusing because one would usually assume to get the size of the vector terms when calling size() for this struct, not size of the scores in terms. Maybe we could rename the function to have it more explicit?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't mind either way, but it is consistent in the view that it returns the maximum index + 1 for the array accessor below. In that sense it is like a vector.


std::vector<DenseScoreTerm> denseScoreTerms(denseScores->terms.begin(), denseScores->terms.end());
for (auto& term : denseScoreTerms) {
term.scale *= scale_;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why *= and not =?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Because the terms might come from a combined label scorer that does scaling differently for each term. Then the scaled label scorer here should multiply its own scale and not override the scales of the combined label scorer.

Comment on lines +427 to +432
if (denseScores and tokenIdx < denseScores->size()) {
extScore += (*denseScores)[tokenIdx];
}
else {
extScore += (*scoreAccessor)->getScore(transitionType, tokenIdx);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do you use an if-else block here and not the same syntax as below?

else if (denseScores->size() != denseSize) {
return std::nullopt;
}
denseScoreTerms.insert(denseScoreTerms.end(), denseScores->terms.begin(), denseScores->terms.end());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't get the implementation for this ScoreAccessor. Why do you just collect the dense score terms of all subAccessors in one vector? Don't we need to sum them up?

Comment on lines +44 to +46
size_t size() const {
return terms.empty() ? 0ul : terms.front().scores.size();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't mind either way, but it is consistent in the view that it returns the maximum index + 1 for the array accessor below. In that sense it is like a vector.

std::vector<DenseScoreTerm> terms;

DenseScoreSpan(std::vector<DenseScoreTerm>&& terms)
: terms(std::move(terms)) {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe we could add an assertion here to check that all terms are of the same length.


std::vector<DenseScoreTerm> denseScoreTerms(denseScores->terms.begin(), denseScores->terms.end());
for (auto& term : denseScoreTerms) {
term.scale *= scale_;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Because the terms might come from a combined label scorer that does scaling differently for each term. Then the scaled label scorer here should multiply its own scale and not override the scales of the combined label scorer.

for (auto& term : denseScoreTerms) {
term.scale *= scale_;
}
return DenseScoreSpan(std::move(denseScoreTerms));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we need to create a new DenseScoreSpan here instead of returning the modified one?

return std::nullopt;
}

std::vector<Nn::DenseScoreTerm> denseScoreTerms(denseScores->terms.begin(), denseScores->terms.end());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
std::vector<Nn::DenseScoreTerm> denseScoreTerms(denseScores->terms.begin(), denseScores->terms.end());
std::vector<Nn::DenseScoreTerm> denseScoreTerms(denseScores->terms.begin(), denseScores->terms.end());

But actually: why do we need a copy? We could also just update denseScores, no?

Comment on lines +56 to +58
Core::Ref<ScoreAccessor> scoreAccessor_;
const bool negateInput_;
std::shared_ptr<Nn::Prior<Score>> prior_;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why this whitespace change?

denseScoreTerms.push_back(Nn::DenseScoreTerm{denseScores->size() > 0ul ? std::span<Score const>(&prior_->at(0), prior_->size()) : std::span<Score const>(), prior_->scale()});
}

return Nn::DenseScoreSpan(std::move(denseScoreTerms));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why create a new DenseScoreSpan instead of returning the modified one?

auto const& scoreAccessor = scoreAccessors[hypIndexToContextIndexMap_[ext.baseHypIndex]];

if (scoreAccessor) {
ext.score += (*scoreAccessor)->getScore(ext.transitionType, ext.nextToken);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shouldn't we also use the dense scores here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants