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
12 changes: 12 additions & 0 deletions packages/inference/src/schemas/llamacpp-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ export const llmConfigBaseSchema = z.object({
* Ignored by text-only models. Default is `"sequential"`.
*/
image_tile_mode: z.enum(['disabled', 'batched', 'sequential']).optional(),
/**
* idefics3-style image preprocessing rule (multimodal models only):
* - `"on"`: round the image's long side up to a whole number of slices and
* cap it, so an image smaller than the cap keeps its own resolution and
* becomes far fewer slices.
* - `"off"`: always stretch the long side to the cap.
* When unset, the model's own GGUF value is used. Ignored with a warning by
* models that do not use idefics3-style preprocessing. Changes the number of
* image tokens, and therefore both accuracy and encode time, so a checkpoint
* whose GGUF omits the key needs this set to preprocess correctly.
*/
image_no_upscale: z.enum(['on', 'off']).optional(),
/**
* Run the multimodal projector (mmproj / vision encoder) on the GPU
* (multimodal models only). `true` forces GPU, `false` forces CPU. When
Expand Down
28 changes: 28 additions & 0 deletions packages/inference/test/llm-config-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,34 @@ test('llmConfigSchema: explicit image_tile_mode overrides the default', (t) => {
if (result.success) t.is(result.data.image_tile_mode, 'batched')
})

test('llmConfigBaseSchema: accepts valid image_no_upscale values', (t) => {
t.is(llmConfigBaseSchema.safeParse({ image_no_upscale: 'on' }).success, true)
t.is(llmConfigBaseSchema.safeParse({ image_no_upscale: 'off' }).success, true)
})

test('llmConfigBaseSchema: rejects invalid image_no_upscale values', (t) => {
t.is(llmConfigBaseSchema.safeParse({ image_no_upscale: true }).success, false)
t.is(llmConfigBaseSchema.safeParse({ image_no_upscale: 1 }).success, false)
t.is(llmConfigBaseSchema.safeParse({ image_no_upscale: 'yes' }).success, false)
})

// Unset must stay unset. The addon reads absence as fabric's -1 sentinel, meaning
// "use the model's own GGUF value"; a default here would force one rule on every
// model and silently change preprocessing for existing callers.
test('llmConfigBaseSchema: image_no_upscale is optional and has no default', (t) => {
t.is(llmConfigBaseSchema.safeParse({}).success, true)
const result = llmConfigSchema.safeParse({})
t.is(result.success, true)
if (result.success) t.is(result.data.image_no_upscale, undefined)
})

// The regression this guards: load-model.ts validates modelConfig with
// llmConfigBaseSchema.strict(), so a field present in the SDK copy of this schema but
// missing here is rejected before it ever reaches the addon.
test('loadBuiltinModelOptions: strict validation admits image_no_upscale', (t) => {
t.is(llmConfigBaseSchema.strict().safeParse({ image_no_upscale: 'on' }).success, true)
})

test('llmConfigBaseSchema: accepts mmproj-use-gpu boolean', (t) => {
const enabled = llmConfigBaseSchema.safeParse({ 'mmproj-use-gpu': true })
t.is(enabled.success, true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@
LoadModelSrcRequestLlamacppCompletion,
LoadModelSrcRequestLlamacppCompletionDelegate,
LoadModelSrcRequestLlamacppCompletionModelConfig,
LoadModelSrcRequestLlamacppCompletionModelConfigImageNoUpscale,
LoadModelSrcRequestLlamacppCompletionModelConfigImageTileMode,
LoadModelSrcRequestLlamacppCompletionModelConfigMainGpu,
LoadModelSrcRequestLlamacppCompletionModelConfigProjectionModelSrc,
Expand Down Expand Up @@ -1275,6 +1276,7 @@
"LoadModelSrcRequestLlamacppCompletion",
"LoadModelSrcRequestLlamacppCompletionDelegate",
"LoadModelSrcRequestLlamacppCompletionModelConfig",
"LoadModelSrcRequestLlamacppCompletionModelConfigImageNoUpscale",
"LoadModelSrcRequestLlamacppCompletionModelConfigImageTileMode",
"LoadModelSrcRequestLlamacppCompletionModelConfigMainGpu",
"LoadModelSrcRequestLlamacppCompletionModelConfigProjectionModelSrc",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6625,6 +6625,11 @@ class LoadModelSrcRequestLlamacppCompletionModelConfigImageTileMode(Enum):
sequential = "sequential"


class LoadModelSrcRequestLlamacppCompletionModelConfigImageNoUpscale(Enum):
on = "on"
off = "off"


class LoadModelSrcRequestLlamacppCompletionModelConfig(GeneratedBaseModel):
ctx_size: float | None = None
temp: Annotated[float | None, Field(ge=0.0, le=2.0)] = None
Expand Down Expand Up @@ -6672,6 +6677,10 @@ class LoadModelSrcRequestLlamacppCompletionModelConfig(GeneratedBaseModel):
LoadModelSrcRequestLlamacppCompletionModelConfigImageTileMode | None,
Field(title="LoadModelSrcRequestLlamacppCompletionModelConfigImageTileMode"),
] = None
image_no_upscale: Annotated[
LoadModelSrcRequestLlamacppCompletionModelConfigImageNoUpscale | None,
Field(title="LoadModelSrcRequestLlamacppCompletionModelConfigImageNoUpscale"),
] = None
mmproj_use_gpu: Annotated[bool | None, Field(alias="mmproj-use-gpu")] = None


Expand Down
5 changes: 5 additions & 0 deletions packages/sdk/contract/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -8264,6 +8264,11 @@
"enum": ["disabled", "batched", "sequential"],
"title": "LoadModelSrcRequestLlamacppCompletionModelConfigImageTileMode"
},
"image_no_upscale": {
"type": "string",
"enum": ["on", "off"],
"title": "LoadModelSrcRequestLlamacppCompletionModelConfigImageNoUpscale"
},
"mmproj-use-gpu": {
"type": "boolean"
}
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/e2e/tests/batch-completion-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const markerDeterministic: GenerationParams = { ...deterministic, predict: 32 }
const visionDeterministic: GenerationParams = {
temp: 0,
seed: 42,
predict: 48
predict: 128
}
const ELEPHANT_IMAGE_TERMS = ['elephant', 'tusk', 'trunk']

Expand Down
24 changes: 18 additions & 6 deletions packages/sdk/e2e/tests/desktop/consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ import {
PI05_BASE_Q_AGGRESSIVE,
GROOT_Q5_VF16,
GROOT_MULTI_Q5_VF16,
SMOLVLM2_500M_MULTIMODAL_Q8_0,
MMPROJ_SMOLVLM2_500M_MULTIMODAL_Q8_0,
VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M,
MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0,
FLUX_2_KLEIN_4B_Q4_0,
FLUX_2_KLEIN_4B_VAE,
QWEN3_4B_Q4_K_M,
Expand Down Expand Up @@ -482,21 +482,33 @@ resources.define('bci', {
})

resources.define('vision', {
constant: SMOLVLM2_500M_MULTIMODAL_Q8_0,
constant: VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M,
type: 'llamacpp-completion',
config: {
ctx_size: 4096,
projectionModelSrc: MMPROJ_SMOLVLM2_500M_MULTIMODAL_Q8_0
image_no_upscale: 'on',
projectionModelSrc: MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0
}
})

resources.define('vision-batch', {
constant: SMOLVLM2_500M_MULTIMODAL_Q8_0,
constant: VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M,
type: 'llamacpp-completion',
config: {
ctx_size: 2048,
parallel: 2,
projectionModelSrc: MMPROJ_SMOLVLM2_500M_MULTIMODAL_Q8_0
image_no_upscale: 'on',
projectionModelSrc: MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0
}
})

resources.define('vision-upscale', {
constant: VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M,
type: 'llamacpp-completion',
config: {
ctx_size: 4096,
image_no_upscale: 'off',
projectionModelSrc: MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0
}
})

Expand Down
26 changes: 19 additions & 7 deletions packages/sdk/e2e/tests/electron/consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ import {
PARAKEET_INDIC_CONFORMER_CTC_Q4_0,
PARAKEET_SORTFORMER_4SPK_V2_1_Q4_0,
PARAKEET_EOU_120M_V1_Q4_0,
SMOLVLM2_500M_MULTIMODAL_Q8_0,
MMPROJ_SMOLVLM2_500M_MULTIMODAL_Q8_0,
VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M,
MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0,
QWEN3_5_0_8B_MULTIMODAL_Q4_K_M,
GEMMA4_2B_MULTIMODAL_Q4_K_M,
BCI_WINDOWED
Expand Down Expand Up @@ -434,21 +434,33 @@ resources.define('bci', {
})

resources.define('vision', {
constant: SMOLVLM2_500M_MULTIMODAL_Q8_0,
constant: VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M,
type: 'llamacpp-completion',
config: {
ctx_size: 1024,
projectionModelSrc: MMPROJ_SMOLVLM2_500M_MULTIMODAL_Q8_0
ctx_size: 4096,
image_no_upscale: 'on',
projectionModelSrc: MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0
}
})

resources.define('vision-batch', {
constant: SMOLVLM2_500M_MULTIMODAL_Q8_0,
constant: VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M,
type: 'llamacpp-completion',
config: {
ctx_size: 2048,
parallel: 2,
projectionModelSrc: MMPROJ_SMOLVLM2_500M_MULTIMODAL_Q8_0
image_no_upscale: 'on',
projectionModelSrc: MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0
}
})

resources.define('vision-upscale', {
constant: VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M,
type: 'llamacpp-completion',
config: {
ctx_size: 4096,
image_no_upscale: 'off',
projectionModelSrc: MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0
}
})

Expand Down
24 changes: 18 additions & 6 deletions packages/sdk/e2e/tests/mobile/consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ import {
PARAKEET_CTC_0_6B_Q4_0,
PARAKEET_SORTFORMER_4SPK_V2_1_Q4_0,
PARAKEET_EOU_120M_V1_Q4_0,
SMOLVLM2_500M_MULTIMODAL_Q8_0,
MMPROJ_SMOLVLM2_500M_MULTIMODAL_Q8_0,
VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M,
MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0,
SMOLVLA_LIBERO_VISION_Q8
} from '@qvac/sdk'
import { ResourceManager } from '../shared/resource-manager.js'
Expand Down Expand Up @@ -415,21 +415,33 @@ resources.define('parakeet-eou', {
})

resources.define('vision', {
constant: SMOLVLM2_500M_MULTIMODAL_Q8_0,
constant: VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M,
type: 'llamacpp-completion',
config: {
ctx_size: 4096,
projectionModelSrc: MMPROJ_SMOLVLM2_500M_MULTIMODAL_Q8_0
image_no_upscale: 'on',
projectionModelSrc: MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0
}
})

resources.define('vision-batch', {
constant: SMOLVLM2_500M_MULTIMODAL_Q8_0,
constant: VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M,
type: 'llamacpp-completion',
config: {
ctx_size: 2048,
parallel: 2,
projectionModelSrc: MMPROJ_SMOLVLM2_500M_MULTIMODAL_Q8_0
image_no_upscale: 'on',
projectionModelSrc: MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0
}
})

resources.define('vision-upscale', {
constant: VISIONPSY_NANO_460M_MULTIMODAL_Q4_K_M,
type: 'llamacpp-completion',
config: {
ctx_size: 4096,
image_no_upscale: 'off',
projectionModelSrc: MMPROJ_VISIONPSY_NANO_460M_MULTIMODAL_Q8_0
}
})

Expand Down
52 changes: 52 additions & 0 deletions packages/sdk/e2e/tests/mobile/executors/vision-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export class MobileVisionExecutor extends ModelAssetExecutor<typeof visionTests>

protected handlers = Object.fromEntries(
visionTests.map((test) => {
if (test.testId === 'vision-image-no-upscale') {
return [test.testId, this.imageNoUpscale.bind(this)]
}
if (test.testId.endsWith('-streaming')) {
return [test.testId, this.streaming.bind(this)]
}
Expand All @@ -33,6 +36,55 @@ export class MobileVisionExecutor extends ModelAssetExecutor<typeof visionTests>
super(resources)
}

private async getPromptTokens(modelId: string, params: VisionParams) {
const history = await this.resolveAttachments(params.history)
return callWhenAddonIdle(async () => {
const result = completion({
modelId,
history,
stream: false,
...(params.generationParams && { generationParams: params.generationParams })
} as never)
await result.text
const stats = await result.stats
if (typeof stats?.promptTokens !== 'number' || stats.promptTokens <= 0) {
throw new Error(`Completion stats missing promptTokens. Got: ${JSON.stringify(stats)}`)
}
return stats.promptTokens
})
}

async imageNoUpscale(params: unknown, _expectation: Expectation): Promise<TestResult> {
const p = params as VisionParams

try {
const noUpscaleModelId = await this.resources.ensureLoaded('vision')
const noUpscaleTokens = await this.getPromptTokens(noUpscaleModelId, p)

await this.resources.evict('vision')

const upscaleModelId = await this.resources.ensureLoaded('vision-upscale')
const upscaleTokens = await this.getPromptTokens(upscaleModelId, p)

if (noUpscaleTokens * 2 >= upscaleTokens) {
return {
passed: false,
output:
`image_no_upscale on (${noUpscaleTokens}) should use less than half the prompt tokens ` +
`of off (${upscaleTokens})`
}
}

return {
passed: true,
output: `image_no_upscale reduced prompt tokens from ${upscaleTokens} to ${noUpscaleTokens}`
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
return { passed: false, output: `image_no_upscale comparison failed: ${errorMsg}` }
}
}

private async loadImageAssets() {
if (!this.imageAssets) {
// @ts-ignore - assets.ts is generated at consumer build time
Expand Down
52 changes: 52 additions & 0 deletions packages/sdk/e2e/tests/shared/executors/node/vision-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export class VisionExecutor extends AbstractModelExecutor<typeof visionTests> {

protected handlers = Object.fromEntries(
visionTests.map((test) => {
if (test.testId === 'vision-image-no-upscale') {
return [test.testId, this.imageNoUpscale.bind(this)]
}
if (test.testId.endsWith('-streaming')) {
return [test.testId, this.streaming.bind(this)]
}
Expand All @@ -26,6 +29,55 @@ export class VisionExecutor extends AbstractModelExecutor<typeof visionTests> {
})
) as never

private async getPromptTokens(modelId: string, params: VisionParams) {
const history = this.resolveAttachments(params.history)
return callWhenAddonIdle(async () => {
const result = completion({
modelId,
history,
stream: false,
...(params.generationParams && { generationParams: params.generationParams })
} as never)
await result.text
const stats = await result.stats
if (typeof stats?.promptTokens !== 'number' || stats.promptTokens <= 0) {
throw new Error(`Completion stats missing promptTokens. Got: ${JSON.stringify(stats)}`)
}
return stats.promptTokens
})
}

async imageNoUpscale(params: unknown, _expectation: Expectation): Promise<TestResult> {
const p = params as VisionParams

try {
const noUpscaleModelId = await this.resources.ensureLoaded('vision')
const noUpscaleTokens = await this.getPromptTokens(noUpscaleModelId, p)

await this.resources.evict('vision')

const upscaleModelId = await this.resources.ensureLoaded('vision-upscale')
const upscaleTokens = await this.getPromptTokens(upscaleModelId, p)

if (noUpscaleTokens * 2 >= upscaleTokens) {
return {
passed: false,
output:
`image_no_upscale on (${noUpscaleTokens}) should use less than half the prompt tokens ` +
`of off (${upscaleTokens})`
}
}

return {
passed: true,
output: `image_no_upscale reduced prompt tokens from ${upscaleTokens} to ${noUpscaleTokens}`
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
return { passed: false, output: `image_no_upscale comparison failed: ${errorMsg}` }
}
}

private resolveAttachments(history: VisionParams['history']) {
return history.map((msg) => {
if (!msg.attachments?.length) return msg
Expand Down
Loading
Loading