fix(experimental): weight turn cache hit by measured prompt tokens
This commit is contained in:
1 parent
1d751809a1
commit
559c4c689d
8 files changed
+57
-27
No files matched your search
@@ -24,8 +24,8 @@
|
||||
en: e001601f2f9779b1
|
||||
zh: 077d3703b8c82774
|
||||
/deepseek-ai-dsh-experimental-client-ui-perf/understand-the-implementation:
|
||||
en: 289e8a539820fa4d
|
||||
zh: 5dad2cfac77761e8
|
||||
en: 611ac4f20831030d
|
||||
zh: 11051434b5271feb
|
||||
/deepseek-ai-dsh-experimental-client-ui-perf/further-exploration:
|
||||
en: 285cfe1d7a99af34
|
||||
zh: fbcee76e0c2f20fb
|
||||
|
||||
@@ -57,6 +57,8 @@ Four Conversation Definitions fold the event window into nodes for one view targ
|
||||
| `src/client/PerfView.tsx` | Scrollport with scroll-to-top paging, table, time bars, step accordion, copy controls, and the prompt image pill with its preview and viewer |
|
||||
| `src/client/locales.ts` | Every visible string, in Chinese, English, and Japanese |
|
||||
|
||||
The turn cache-hit cell is token-weighted across the steps that reported usage (summed cache reads ÷ summed prompt tokens), so one transient cold step is not hidden by a warmer neighbour; the tooltip shows the summed read/input/write breakdown. A step row keeps its own measured share.
|
||||
|
||||
Windows are bounded: when a step's `step/start` is outside the loaded window, its start-dependent columns show `—` with a `start unknown` marker rather than a guessed value. Scrolling to the top asks the Session for one older page; the view offsets the scroll position by the height it prepended, so the row being read stays in place.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -57,6 +57,8 @@ kind: "package-reference"
|
||||
| `src/client/PerfView.tsx` | 表格、时间条、步骤手风琴、复制按钮,以及提问图片标签、预览与看图器 |
|
||||
| `src/client/locales.ts` | 全部可见文案,中、英、日三份 |
|
||||
|
||||
轮次的缓存命中率按报了 usage 的各步做 token 加权(缓存读之和 ÷ 提示词 token 之和),不会因为相邻步命中高就把某一次冷启动盖掉;tooltip 展示汇总后的读/输入/写明细。步骤行仍显示该步自己的实测比例。
|
||||
|
||||
会话窗口是有界的:某一步的 `step/start` 不在已加载窗口里时,与起点有关的列显示 `—` 并标「起止未知」,不估算数值。滚到顶部会向 Session 请求更早的一页;视图按新增内容的高度补偿滚动位置,正在读的行不会跳走。
|
||||
|
||||
</details>
|
||||
|
||||
@@ -150,6 +150,8 @@ interface MetricsValues {
|
||||
readonly tool: number | null
|
||||
readonly cacheRead: number | null
|
||||
readonly cacheWrite: number | null
|
||||
/** Weighted prompt denominator of a turn's cache rate; step rows use `context`. */
|
||||
readonly cacheContext?: number | null
|
||||
readonly ttftPending?: number
|
||||
readonly modelPending?: number
|
||||
readonly toolPending?: number
|
||||
@@ -802,7 +804,9 @@ const COLUMN_DETAIL: Readonly<
|
||||
if (!isNumber(value.cacheRead) && !isNumber(value.cacheWrite)) return t('column.cache.title')
|
||||
const read = value.cacheRead ?? 0
|
||||
const write = value.cacheWrite ?? 0
|
||||
const context = isNumber(value.context) ? value.context : read + write
|
||||
const context = isNumber(value.cacheContext)
|
||||
? value.cacheContext
|
||||
: isNumber(value.context) ? value.context : read + write
|
||||
const input = Math.max(0, context - read - write)
|
||||
return t('cache.detail', {
|
||||
read: formatTokens(read),
|
||||
|
||||
@@ -44,16 +44,26 @@ function turnContext(steps: readonly PerfStepMetrics[]): number | null {
|
||||
return context
|
||||
}
|
||||
|
||||
/** Cache breakdown measured last in the turn, from the same step whose hit rate the row shows. */
|
||||
function turnCache(steps: readonly PerfStepMetrics[]): { read: number | null; write: number | null } {
|
||||
let read: number | null = null
|
||||
let write: number | null = null
|
||||
/** Token-weighted cache level of the turn, over the steps that reported both read and prompt tokens. */
|
||||
function turnCache(steps: readonly PerfStepMetrics[]): {
|
||||
rate: number | null
|
||||
read: number | null
|
||||
write: number | null
|
||||
context: number | null
|
||||
} {
|
||||
let read = 0
|
||||
let write = 0
|
||||
let context = 0
|
||||
let measured = false
|
||||
for (const step of steps) {
|
||||
if (!isNumber(step.cacheHit)) continue
|
||||
read = step.cacheRead
|
||||
write = step.cacheWrite
|
||||
if (!isNumber(step.cacheRead) || !isNumber(step.context)) continue
|
||||
read += step.cacheRead
|
||||
write += step.cacheWrite ?? 0
|
||||
context += step.context
|
||||
measured = true
|
||||
}
|
||||
return { read, write }
|
||||
const level = measured && context > 0 ? Math.min(1, read / context) : null
|
||||
return { rate: level, read: measured ? read : null, write: measured ? write : null, context: measured ? context : null }
|
||||
}
|
||||
|
||||
/** Tool tallies by descending count; ties keep call order. */
|
||||
@@ -144,16 +154,12 @@ export function aggregateTurn(turn: number, steps: readonly PerfStepMetrics[]):
|
||||
&& !last.running && isNumber(first.startTime) && isNumber(last.endTime)
|
||||
? Math.max(0, last.endTime - first.startTime)
|
||||
: null
|
||||
// Last cache hit measured in the turn: the prompt grows, so the tail value is
|
||||
// the turn's actual level rather than the first step's cold start.
|
||||
let cacheHit: number | null = null
|
||||
let answer: string | null = null
|
||||
let retries = 0
|
||||
let toolErrors = 0
|
||||
let toolOutput = 0
|
||||
let lastStepWall: number | null = null
|
||||
for (const step of steps) {
|
||||
if (isNumber(step.cacheHit)) cacheHit = step.cacheHit
|
||||
if (step.answer !== null) answer = step.answer
|
||||
retries += step.retries
|
||||
toolErrors += step.toolErrors
|
||||
@@ -182,9 +188,10 @@ export function aggregateTurn(turn: number, steps: readonly PerfStepMetrics[]):
|
||||
toolWall: toolWallMeasured ? toolWallSum : null,
|
||||
toolPending: toolPairable === 0 ? 0 : toolPending,
|
||||
tools: toolCounts,
|
||||
cacheHit,
|
||||
cacheHit: cache.rate,
|
||||
cacheRead: cache.read,
|
||||
cacheWrite: cache.write,
|
||||
cacheContext: cache.context,
|
||||
retries,
|
||||
toolErrors,
|
||||
toolOutput,
|
||||
|
||||
@@ -139,10 +139,14 @@ export interface PerfTurnMetrics {
|
||||
readonly toolWall: number | null
|
||||
readonly toolPending: number
|
||||
readonly tools: readonly PerfToolCount[]
|
||||
/** Token-weighted cache-read share across the steps that reported usage. */
|
||||
readonly cacheHit: number | null
|
||||
/** Cache breakdown of the last step that reported usage, for the turn tooltip. */
|
||||
/** Cache-read tokens summed across the steps that reported usage. */
|
||||
readonly cacheRead: number | null
|
||||
/** Cache-write tokens summed across the steps that reported usage. */
|
||||
readonly cacheWrite: number | null
|
||||
/** Prompt tokens summed across the steps that reported usage; the weight of `cacheHit`. */
|
||||
readonly cacheContext: number | null
|
||||
readonly retries: number
|
||||
readonly toolErrors: number
|
||||
readonly toolOutput: number
|
||||
|
||||
@@ -204,30 +204,35 @@ describe('turn aggregation', () => {
|
||||
expect(noOutput.tps).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps the last measured context and cache level of the turn', () => {
|
||||
it('weights the turn cache level by measured prompt tokens and keeps the last context', () => {
|
||||
const turn = aggregateTurn(4, [
|
||||
metricsOf({ step: 1, context: 100, cacheHit: 0.2, cacheRead: 10, cacheWrite: 5, wall: 10 }),
|
||||
metricsOf({ step: 2, context: 200, cacheHit: 0.5, cacheRead: 20, cacheWrite: 6, wall: 20 }),
|
||||
metricsOf({ step: 1, context: 100, cacheHit: 0.2, cacheRead: 20, cacheWrite: 5, wall: 10 }),
|
||||
metricsOf({ step: 2, context: 200, cacheHit: 0.5, cacheRead: 100, cacheWrite: 6, wall: 20 }),
|
||||
metricsOf({ step: 3, context: 300, wall: 30 }),
|
||||
])
|
||||
expect(turn.context).toBe(300)
|
||||
expect(turn.cacheHit).toBe(0.5)
|
||||
expect(turn.cacheRead).toBe(20)
|
||||
expect(turn.cacheWrite).toBe(6)
|
||||
expect(turn.cacheHit).toBeCloseTo(120 / 300)
|
||||
expect(turn.cacheRead).toBe(120)
|
||||
expect(turn.cacheWrite).toBe(11)
|
||||
expect(turn.cacheContext).toBe(300)
|
||||
expect(turn.lastStepWall).toBe(30)
|
||||
const heldBack = aggregateTurn(4, [
|
||||
metricsOf({ step: 1, context: 100, cacheHit: 0.2, cacheRead: 10, cacheWrite: 5, wall: 10 }),
|
||||
metricsOf({ step: 1, context: 100, cacheHit: 0.2, cacheRead: 20, cacheWrite: 5, wall: 10 }),
|
||||
metricsOf({ step: 2, cacheHit: null, cacheRead: 99, cacheWrite: 99, wall: null }),
|
||||
])
|
||||
expect(heldBack.context).toBe(100)
|
||||
expect(heldBack.cacheHit).toBe(0.2)
|
||||
expect(heldBack.cacheRead).toBe(10)
|
||||
expect(heldBack.cacheRead).toBe(20)
|
||||
expect(heldBack.cacheWrite).toBe(5)
|
||||
expect(heldBack.cacheContext).toBe(100)
|
||||
expect(heldBack.lastStepWall).toBe(10)
|
||||
const bare = aggregateTurn(4, [metricsOf({ cacheHit: 0.4 })])
|
||||
expect(bare.cacheHit).toBe(0.4)
|
||||
expect(bare.cacheHit).toBeNull()
|
||||
expect(bare.cacheRead).toBeNull()
|
||||
expect(bare.cacheWrite).toBeNull()
|
||||
expect(bare.cacheContext).toBeNull()
|
||||
const empty = aggregateTurn(4, [metricsOf({ context: 0, cacheRead: 0 })])
|
||||
expect(empty.cacheHit).toBeNull()
|
||||
})
|
||||
|
||||
it('sums the sequential step tool spans', () => {
|
||||
|
||||
@@ -124,6 +124,7 @@ function turn(overrides: Partial<PerfTurnMetrics> = {}): PerfTurnMetrics {
|
||||
cacheHit: null,
|
||||
cacheRead: null,
|
||||
cacheWrite: null,
|
||||
cacheContext: null,
|
||||
retries: 0,
|
||||
toolErrors: 0,
|
||||
toolOutput: 0,
|
||||
@@ -728,8 +729,9 @@ describe('turn rows', () => {
|
||||
turn({ turn: 2, cacheHit: 0.1, cacheRead: null, cacheWrite: 200, context: 1_000 }),
|
||||
turn({ turn: 3, cacheHit: 0.4, cacheRead: 100, cacheWrite: null, context: null }),
|
||||
turn({ turn: 4, cacheHit: 0.2, cacheRead: null, cacheWrite: null }),
|
||||
turn({ turn: 5, cacheHit: 0.15, cacheRead: 100, cacheWrite: 200, cacheContext: 2_000 }),
|
||||
],
|
||||
stepCount: 4,
|
||||
stepCount: 5,
|
||||
})
|
||||
const full = cellsOf(rowWithPrefix(say('turn.label', { turn: 1 })))
|
||||
expect(full[5]?.getAttribute('title')).toBe(say('cache.detail', {
|
||||
@@ -747,6 +749,10 @@ describe('turn rows', () => {
|
||||
expect(readOnly[3]?.textContent).toBe('—')
|
||||
const absent = cellsOf(rowWithPrefix(say('turn.label', { turn: 4 })))
|
||||
expect(absent[5]?.getAttribute('title')).toBe(say('column.cache.title'))
|
||||
const weighted = cellsOf(rowWithPrefix(say('turn.label', { turn: 5 })))
|
||||
expect(weighted[5]?.getAttribute('title')).toBe(say('cache.detail', {
|
||||
read: formatTokens(100), input: formatTokens(1_700), write: formatTokens(200),
|
||||
}))
|
||||
})
|
||||
|
||||
it('scales turn bars against the turns visible on screen', () => {
|
||||
|
||||
Reference in new issue
Block a user