From 366b61e52fd7db422c7030647604685b2b115f52 Mon Sep 17 00:00:00 2001 From: Dishit Date: Mon, 20 Apr 2026 20:38:49 +0530 Subject: [PATCH 1/4] fix(threads): remove runaway nThreads migration and show resolved Auto count migratePersistedState was resetting any explicit nThreads=4 back to Auto on every app open, silently overriding the user's choice. Deleted the unguarded migration block. Also resolves the Auto sentinel to its actual hardware thread count (cores <= 4 ? cores : floor(cores * 0.8)) so the settings slider shows "Auto (4)" instead of just "Auto". --- __tests__/rntl/screens/ModelSettingsScreen.test.tsx | 6 +++--- .../unit/hooks/useTextGenerationAdvanced.test.ts | 6 ++++-- src/hooks/useTextGenerationAdvanced.ts | 13 ++++++++++++- src/stores/appStore.ts | 5 ----- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/__tests__/rntl/screens/ModelSettingsScreen.test.tsx b/__tests__/rntl/screens/ModelSettingsScreen.test.tsx index de00d047..3ec121bf 100644 --- a/__tests__/rntl/screens/ModelSettingsScreen.test.tsx +++ b/__tests__/rntl/screens/ModelSettingsScreen.test.tsx @@ -433,10 +433,10 @@ describe('ModelSettingsScreen', () => { // Performance Settings // ============================================================================ describe('performance settings', () => { - it('shows CPU Threads slider label and auto value when nThreads uses the auto sentinel', () => { - const { getByText } = renderWithSections('text'); + it('shows CPU Threads slider label and auto value when nThreads uses the auto sentinel', async () => { + const { getByText, findByText } = renderWithSections('text'); expect(getByText('CPU Threads')).toBeTruthy(); - expect(getByText('Auto')).toBeTruthy(); + await findByText(/^Auto \(\d+\)$/); }); it('shows Batch Size slider label and default value', () => { diff --git a/__tests__/unit/hooks/useTextGenerationAdvanced.test.ts b/__tests__/unit/hooks/useTextGenerationAdvanced.test.ts index 4c4e0e54..332f4b80 100644 --- a/__tests__/unit/hooks/useTextGenerationAdvanced.test.ts +++ b/__tests__/unit/hooks/useTextGenerationAdvanced.test.ts @@ -21,14 +21,16 @@ describe('useTextGenerationAdvanced', () => { expect(result.current.displayCacheType).toBe('f16'); }); - it('shows Auto for cpu threads when nThreads uses the auto sentinel', () => { + it('shows Auto (N) for cpu threads when nThreads uses the auto sentinel', async () => { act(() => { useAppStore.getState().updateSettings({ nThreads: 0 }); }); const { result } = renderHook(() => useTextGenerationAdvanced()); - expect(result.current.cpuThreadsDisplayValue).toBe('Auto'); + await act(async () => {}); + + expect(result.current.cpuThreadsDisplayValue).toMatch(/^Auto \(\d+\)$/); expect(result.current.cpuThreadsSliderValue).toBe(1); }); }); diff --git a/src/hooks/useTextGenerationAdvanced.ts b/src/hooks/useTextGenerationAdvanced.ts index 173acbda..b058d848 100644 --- a/src/hooks/useTextGenerationAdvanced.ts +++ b/src/hooks/useTextGenerationAdvanced.ts @@ -1,6 +1,8 @@ +import { useState, useEffect } from 'react'; import { Platform } from 'react-native'; import { useAppStore } from '../stores'; import { CacheType, INFERENCE_BACKENDS } from '../types'; +import { hardwareService } from '../services/hardware'; /** Feature flag: Set to true to enable HTP/Hexagon NPU support. Currently disabled. */ const HTP_ENABLED = false; @@ -31,8 +33,17 @@ export function useTextGenerationAdvanced() { // OpenCL and HTP force f16 in the native loader, so lock the UI to match. const cacheDisabled = gpuForcesF16; const displayCacheType = cacheDisabled ? 'f16' : currentCacheType; + const [resolvedThreadCount, setResolvedThreadCount] = useState(null); + + useEffect(() => { + if (settings?.nThreads !== 0) return; + hardwareService.getRecommendedThreadCount().then(setResolvedThreadCount); + }, [settings?.nThreads]); + const cpuThreadsSliderValue = settings?.nThreads && settings.nThreads > 0 ? settings.nThreads : 1; - const cpuThreadsDisplayValue = settings?.nThreads === 0 ? 'Auto' : String(settings?.nThreads ?? 6); + const cpuThreadsDisplayValue = settings?.nThreads === 0 + ? (resolvedThreadCount != null ? `Auto (${resolvedThreadCount})` : 'Auto') + : String(settings?.nThreads ?? 6); const handleFlashAttnToggle = (flashAttn: boolean) => { if (!flashAttn && isQuantizedCache) { diff --git a/src/stores/appStore.ts b/src/stores/appStore.ts index 58a8b901..e7339c58 100644 --- a/src/stores/appStore.ts +++ b/src/stores/appStore.ts @@ -178,11 +178,6 @@ function migratePersistedState(persistedState: any, currentState: AppState): App if (merged.checklistDismissed && merged.onboardingChecklist && !Object.values(merged.onboardingChecklist).every(Boolean)) merged.checklistDismissed = false; migrateEnabledTools(merged); - // nThreads: 4 was the old hard-coded default (before the 0 = auto sentinel). - // Migrate it to 0 so users get hardware-appropriate thread counts on next load. - if (merged.settings?.nThreads === 4) { - merged.settings = { ...merged.settings, nThreads: 0 }; - } return merged as AppState; } From e2622e9a290b10cbee4698425d7e6addc26b68ad Mon Sep 17 00:00:00 2001 From: Dishit Date: Tue, 21 Apr 2026 14:23:21 +0530 Subject: [PATCH 2/4] fix: sync cpuThreads label fallback with slider value When nThreads is undefined, the display value fell back to "6" while the slider sat at 1. Both now use cpuThreadsSliderValue as the source of truth, keeping label and slider in sync. Co-authored-by: Dishit --- src/hooks/useTextGenerationAdvanced.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useTextGenerationAdvanced.ts b/src/hooks/useTextGenerationAdvanced.ts index b058d848..c68887e1 100644 --- a/src/hooks/useTextGenerationAdvanced.ts +++ b/src/hooks/useTextGenerationAdvanced.ts @@ -43,7 +43,7 @@ export function useTextGenerationAdvanced() { const cpuThreadsSliderValue = settings?.nThreads && settings.nThreads > 0 ? settings.nThreads : 1; const cpuThreadsDisplayValue = settings?.nThreads === 0 ? (resolvedThreadCount != null ? `Auto (${resolvedThreadCount})` : 'Auto') - : String(settings?.nThreads ?? 6); + : String(cpuThreadsSliderValue); const handleFlashAttnToggle = (flashAttn: boolean) => { if (!flashAttn && isQuantizedCache) { From c828ca4c9791276861cf264aba127a80d7e0dafc Mon Sep 17 00:00:00 2001 From: Dishit Date: Tue, 21 Apr 2026 14:25:17 +0530 Subject: [PATCH 3/4] fix test Co-authored-by: Dishit hanmadishit74@gmail.com --- __tests__/rntl/screens/ModelSettingsScreen.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/rntl/screens/ModelSettingsScreen.test.tsx b/__tests__/rntl/screens/ModelSettingsScreen.test.tsx index 3ec121bf..8ab29ab0 100644 --- a/__tests__/rntl/screens/ModelSettingsScreen.test.tsx +++ b/__tests__/rntl/screens/ModelSettingsScreen.test.tsx @@ -837,7 +837,7 @@ describe('ModelSettingsScreen', () => { expect(getByText('0.70')).toBeTruthy(); // temperature || 0.7 expect(getByText('0.90')).toBeTruthy(); // topP || 0.9 expect(getByText('1.10')).toBeTruthy(); // repeatPenalty || 1.1 - expect(getByText('6')).toBeTruthy(); // undefined still falls back to 6 + expect(getByText('1')).toBeTruthy(); // undefined falls back to cpuThreadsSliderValue (1) expect(getByText('8')).toBeTruthy(); // imageSteps || 8 expect(getByText('7.5')).toBeTruthy(); // imageGuidanceScale || 7.5 }); From 101b4f07fb0c33dc83698df23c8b0f95585de30d Mon Sep 17 00:00:00 2001 From: Dishit Date: Tue, 21 Apr 2026 14:28:53 +0530 Subject: [PATCH 4/4] fix test Co-authored-by: Dishit hanmadishit74@gmail.com --- __tests__/rntl/screens/ModelSettingsScreen.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__tests__/rntl/screens/ModelSettingsScreen.test.tsx b/__tests__/rntl/screens/ModelSettingsScreen.test.tsx index 8ab29ab0..f2c1131b 100644 --- a/__tests__/rntl/screens/ModelSettingsScreen.test.tsx +++ b/__tests__/rntl/screens/ModelSettingsScreen.test.tsx @@ -832,12 +832,12 @@ describe('ModelSettingsScreen', () => { }, }); - const { getByText } = renderWithSections('image', 'text'); + const { getByText, getAllByText } = renderWithSections('image', 'text'); // Verify fallback values are used expect(getByText('0.70')).toBeTruthy(); // temperature || 0.7 expect(getByText('0.90')).toBeTruthy(); // topP || 0.9 expect(getByText('1.10')).toBeTruthy(); // repeatPenalty || 1.1 - expect(getByText('1')).toBeTruthy(); // undefined falls back to cpuThreadsSliderValue (1) + expect(getAllByText('1').length).toBeGreaterThan(0); // undefined falls back to cpuThreadsSliderValue (1) expect(getByText('8')).toBeTruthy(); // imageSteps || 8 expect(getByText('7.5')).toBeTruthy(); // imageGuidanceScale || 7.5 });