fix(ml): 优化PyTorch模型推理流程与资源管理
- 修改API接口路径从serve切换到common,统一资源调用 - 增加独立推理线程,隔离ArcSoft线程污染,保障FPU状态稳定 - 推理模块预热,降低PyTorch线程初启功率峰值,避免电流保护触发 - 推理前后记录系统内存信息,辅助OOM重启排查 - 主屏相机与副屏人脸识别推理前暂停,规避硬件并发带宽冲突重启 - 控制相机HAL清理时长,确保DMC带宽资源释放后再启动推理 - 异步推理调用增加超时机制,避免长时间卡死 - 推理结束后恢复相机,确保正常工作流程
This commit is contained in:
@@ -1297,7 +1297,24 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
startTime = System.currentTimeMillis()
|
||||
log("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}")
|
||||
//val nameList = FoodModule.queryFood(bitmap)
|
||||
val scoreList = FoodModule.getFoodScoreList(bitmap)
|
||||
// 推理前同时暂停主屏相机和副屏人脸识别,让 ArcSoft 线程进入空闲后再启动 PyTorch
|
||||
withContext(Dispatchers.Main) {
|
||||
shutdownCamera()
|
||||
presentation?.pauseCamera()
|
||||
}
|
||||
// 等待相机 HAL 在内核层(uvcvideo URB / DMC system_status)完成异步清理
|
||||
// 实测:unbindAll() 之后 HAL 仍会在 ~200ms 内写 sysfs_dmc/system_status,
|
||||
// 若此时 PyTorch forward() 同时占用大量 DRAM 带宽,RK3588 DMC 频率切换
|
||||
// 与高带宽访问并发会触发硬件级故障导致设备重启。
|
||||
// 1500ms 给 HAL 足够时间完成 DMC 带宽 hold 释放后再启动推理。
|
||||
Timber.tag(TAG).d("queryFoodData: 等待相机HAL完成DMC清理 (1500ms)...")
|
||||
kotlinx.coroutines.delay(1500)
|
||||
Timber.tag(TAG).d("queryFoodData: 延迟结束,开始 forward()")
|
||||
val scoreList = try {
|
||||
FoodModule.getFoodScoreList(bitmap)
|
||||
} finally {
|
||||
withContext(Dispatchers.Main) { setupCamera() }
|
||||
}
|
||||
val foodName = if (scoreList.isNotEmpty()) {
|
||||
val recDataJson = GsonUtils.toJson(scoreList)
|
||||
log("registerDataChange main,getFoodScoreList耗时:${System.currentTimeMillis() - startTime}")
|
||||
|
||||
@@ -36,13 +36,13 @@ interface ApiServiceV2 {
|
||||
|
||||
@POST
|
||||
suspend fun getFacePage(
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/face/page",
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/face/page",
|
||||
@Body request: Map<String, Long>
|
||||
): ApiResponse<List<FaceVO>?>
|
||||
|
||||
@POST
|
||||
suspend fun getFaceIncrement(
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/serve/face/increment",
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/common/face/increment",
|
||||
@Body request: Map<String, Long>
|
||||
): ApiResponse<List<FaceVO>?>
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.pytorch.IValue
|
||||
import org.pytorch.Module
|
||||
import org.pytorch.Tensor
|
||||
import org.pytorch.torchvision.TensorImageUtils
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
@@ -29,6 +30,9 @@ object FoodModule {
|
||||
// private const val THRESHOLD = 0.0
|
||||
private var module: Module? = null
|
||||
|
||||
// 专用推理线程:与 Kotlin IO 调度器完全隔离,避免 ArcSoft 污染该线程的 FPU 状态(FPSCR)
|
||||
private val inferenceExecutor = java.util.concurrent.Executors.newSingleThreadExecutor()
|
||||
|
||||
// private lateinit var embeddingsList: List<List<Float>>
|
||||
// private lateinit var labelsList: IntArray
|
||||
// private lateinit var classInfo: FoodClassInfo
|
||||
@@ -46,6 +50,23 @@ object FoodModule {
|
||||
withContext(Dispatchers.IO) {
|
||||
val modelPath = copyAssetToCache(context, "best_embedding_model_mobile.pt")
|
||||
module = Module.load(modelPath)
|
||||
// 预热:在相机启动前先跑一次 forward,触发 PyTorch 线程池创建
|
||||
// 在预热前将当前线程优先级调低,使 PyTorch 内部新建的工作线程也继承低优先级
|
||||
// 从而降低后续推理时的瞬间功率,避免触发 PMIC 过流保护
|
||||
Timber.tag(TAG).d("预热 forward() 开始")
|
||||
val warmupTensor = Tensor.fromBlob(
|
||||
FloatArray(1 * 3 * 224 * 224),
|
||||
longArrayOf(1, 3, 224, 224)
|
||||
)
|
||||
inferenceExecutor.submit { module?.forward(IValue.from(warmupTensor)) }.get()
|
||||
context.let { ctx ->
|
||||
val am = ctx.getSystemService(android.content.Context.ACTIVITY_SERVICE)
|
||||
as android.app.ActivityManager
|
||||
val mi = android.app.ActivityManager.MemoryInfo()
|
||||
am.getMemoryInfo(mi)
|
||||
Timber.tag(TAG).d("预热完成后系统可用内存: ${mi.availMem / 1024 / 1024} MB / 总: ${mi.totalMem / 1024 / 1024} MB")
|
||||
}
|
||||
Timber.tag(TAG).d("预热 forward() 完成")
|
||||
//初始化默认重新拉取数据,先清空本地数据
|
||||
val list = ObjectBox.getAll()
|
||||
if (list.isNotEmpty()) {
|
||||
@@ -86,8 +107,28 @@ object FoodModule {
|
||||
if (module == null) {
|
||||
return null
|
||||
}
|
||||
val outputTensor = module?.forward(IValue.from(inputTensor))?.toTensor()
|
||||
return outputTensor?.dataAsFloatArray
|
||||
Timber.tag(TAG).d("module:${module}")
|
||||
val iValue = IValue.from(inputTensor)
|
||||
Timber.tag(TAG).d("inputTensor:${inputTensor}")
|
||||
|
||||
// 推理前记录系统可用内存,排查是否因内存不足触发 OOM 重启
|
||||
MyApp.instance?.let { ctx ->
|
||||
val am = ctx.getSystemService(android.content.Context.ACTIVITY_SERVICE)
|
||||
as android.app.ActivityManager
|
||||
val mi = android.app.ActivityManager.MemoryInfo()
|
||||
am.getMemoryInfo(mi)
|
||||
Timber.tag(TAG).d("forward() 前系统可用内存: ${mi.availMem / 1024 / 1024} MB / 总: ${mi.totalMem / 1024 / 1024} MB, lowMemory=${mi.lowMemory}")
|
||||
}
|
||||
Timber.tag(TAG).d("forward() 开始: ${System.currentTimeMillis()}")
|
||||
val outputIValue = inferenceExecutor.submit<IValue?> { module?.forward(iValue) }
|
||||
.get(10, java.util.concurrent.TimeUnit.SECONDS) ?: return null
|
||||
Timber.tag(TAG).d("forward() 结束: ${System.currentTimeMillis()}")
|
||||
|
||||
|
||||
Timber.tag(TAG).d("outputIValue:${outputIValue}")
|
||||
val outputTensor = outputIValue.toTensor()
|
||||
Timber.tag(TAG).d("outputTensor:${outputTensor}")
|
||||
return outputTensor.dataAsFloatArray
|
||||
} catch (e: OutOfMemoryError) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user