diff --git a/android-native/engine/src/main/java/team/sopa/boar/engine/CpuTopology.kt b/android-native/engine/src/main/java/team/sopa/boar/engine/CpuTopology.kt index b9cad01e..3e7d0059 100644 --- a/android-native/engine/src/main/java/team/sopa/boar/engine/CpuTopology.kt +++ b/android-native/engine/src/main/java/team/sopa/boar/engine/CpuTopology.kt @@ -15,6 +15,22 @@ data class CpuTopology(val cores: List) { return cores.filter { it.maxKhz in keep }.map { it.id } } + /** + * The cores Auto runs on: the fastest frequency groups, adding the next group until there are 4 cores, but never the + * slowest group once there are 2 (the little cores: every thread then waits for the slowest one; on the X6, 6 and 8 + * threads halved generation). 1 prime + 3 big (Dimensity 8300, Snapdragon 870): those 4. 2 + 2 + 4 (Tensor G1/G2, + * Pixel 6 and 7): the 2 + 2 fast ones, where [bigCores] keeps 2. 2 big + 6 little: the 2 big. One group: all of them. + */ + val autoCores: List get() { + val groups = cores.groupBy { it.maxKhz }.toSortedMap(compareByDescending { it }).values.toList() + val keep = ArrayList() + for ((i, group) in groups.withIndex()) { + if (keep.size >= 4 || (i == groups.lastIndex && keep.size >= 2)) break + keep += group + } + return keep.map { it.id }.sorted() + } + /** "4-7" style list for [EngineParams.cpus]. */ fun cpuList(ids: List): String = ids.sorted().joinToString(",") diff --git a/android-native/engine/src/main/java/team/sopa/boar/engine/EngineTweaks.kt b/android-native/engine/src/main/java/team/sopa/boar/engine/EngineTweaks.kt index 22a1ff27..e207f2a7 100644 --- a/android-native/engine/src/main/java/team/sopa/boar/engine/EngineTweaks.kt +++ b/android-native/engine/src/main/java/team/sopa/boar/engine/EngineTweaks.kt @@ -65,7 +65,7 @@ data class EngineTweaks( fun resolve(topology: CpuTopology, totalRamBytes: Long = 0): Resolved { val notes = ArrayList() val n = topology.cores.size.coerceAtLeast(1) - val big = topology.bigCores.ifEmpty { topology.cores.map { it.id } } + val big = topology.autoCores.ifEmpty { topology.cores.map { it.id } } fun clamp(name: String, v: Int, lo: Int, hi: Int): Int = v.coerceIn(lo, hi).also { if (it != v) notes += "$name $v → $it (allowed $lo–$hi)" } val coreIds: List? = when (cores) { diff --git a/android-native/engine/src/test/java/team/sopa/boar/engine/EngineTweaksTest.kt b/android-native/engine/src/test/java/team/sopa/boar/engine/EngineTweaksTest.kt index e738c10e..2b4dac69 100644 --- a/android-native/engine/src/test/java/team/sopa/boar/engine/EngineTweaksTest.kt +++ b/android-native/engine/src/test/java/team/sopa/boar/engine/EngineTweaksTest.kt @@ -24,6 +24,43 @@ class EngineTweaksTest { assertTrue(EngineTweaks().isAuto) } + /** Cores from (count, max kHz) groups, numbered from 0 in the order given (sysfs lists the little cores first). */ + private fun soc(vararg groups: Pair): CpuTopology { + var id = 0 + return CpuTopology(groups.flatMap { (n, khz) -> List(n) { CpuTopology.Core(id++, khz) } }) + } + + @Test fun `Auto threads - 4 on the fast cores of every layout, never the little ones`() { + // Tensor G1 (Pixel 6, 6a, 6 Pro): 4x A55 1.80 + 2x A76 2.25 + 2x X1 2.80 GHz. bigCores keeps only the 2 X1. + val g1 = soc(4 to 1_803_000L, 2 to 2_253_000L, 2 to 2_802_000L) + // Tensor G2 (Pixel 7, 7a, 7 Pro): 4x A55 1.80 + 2x A78 2.35 + 2x X1 2.85 GHz. + val g2 = soc(4 to 1_803_000L, 2 to 2_348_000L, 2 to 2_850_000L) + // Snapdragon 870 (POCO F3): 4x A55 1.80 + 3x A77 2.42 + 1x A77 3.19 GHz. + val sd870 = soc(4 to 1_804_800L, 3 to 2_419_200L, 1 to 3_187_200L) + // Tensor G3 (Pixel 8): 4x A510 1.70 + 4x A715 2.45 + 1x X3 2.91 GHz. + val g3 = soc(4 to 1_704_000L, 4 to 2_450_000L, 1 to 2_910_000L) + // 6 little + 2 big (Helio G99 class). + val sixTwo = soc(6 to 2_000_000L, 2 to 2_200_000L) + // One cluster, or no frequency readable. + val flat = soc(8 to 0L) + + assertEquals(listOf(6, 7), g1.bigCores) + assertEquals(listOf(4, 5, 6, 7), g1.autoCores) + assertEquals(listOf(4, 5, 6, 7), g2.autoCores) + assertEquals(listOf(4, 5, 6, 7), sd870.autoCores) + assertEquals(listOf(4, 5, 6, 7), x6.autoCores) + assertEquals(listOf(4, 5, 6, 7, 8), g3.autoCores) + assertEquals(listOf(6, 7), sixTwo.autoCores) + assertEquals((0..7).toList(), flat.autoCores) + + for (t in listOf(g1, g2, sd870, x6, g3, flat)) assertEquals(4, EngineTweaks().resolve(t).params.threads) + assertEquals("4,5,6,7", EngineTweaks().resolve(g1).params.cpus) + assertEquals(2, EngineTweaks().resolve(sixTwo).params.threads) + // The user's own setting still wins. + assertEquals(2, EngineTweaks(threads = 2, cores = "6,7").resolve(g1).params.threads) + assertEquals("6,7", EngineTweaks(threads = 2, cores = "6,7").resolve(g1).params.cpus) + } + @Test fun `every knob can be set, and impossible values are clamped with a note`() { val r = EngineTweaks(threads = 64, cores = "all", contextTokens = 100, batch = 256, microBatch = 1024, kvCache = KvType.Q8_0, flashAttention = FlashAttn.ON).resolve(x6) assertEquals(8, r.params.threads)