返回

限制 Piper TTS CPU 占用: 多线程与核心控制方案

Linux

限制 Piper TTS 的 CPU 核心使用

在 Linux 环境下,使用 taskset 命令或 psutil 库来限制 Python 程序对 CPU 核心的使用,是很常见的操作。然而,某些库,例如 Piper TTS,似乎会绕过这些限制。本文将探讨这个问题的原因,并提供一些解决方案。

问题分析

taskset 命令通过修改进程的 CPU 亲和性掩码,来限制进程可以在哪些 CPU 核心上运行。 psutil 库也提供了类似的功能,通过 psutil.Process().cpu_affinity() 方法可以设置进程的亲和性。 理论上,只要正确设置了亲和性,进程就应该只在指定的 CPU 核心上运行。

但实际上,当程序使用 Piper TTS 库进行文本转语音时,通过 taskset 命令或 psutil 库设置的 CPU 核心限制,似乎失效了。 与其他库(比如 llama-cpp-python )的程序相比,Piper TTS 的行为显得异常。这是因为 tasksetpsutil 操作的是进程级别的 CPU 亲和性, 而 Piper TTS 在一些场景下,可能在其内部使用了线程或其他形式的并发,这些内部并发组件可能继承,也可能没有继承主进程设置的 CPU 亲和性。

解决方案

为了解决这个问题,需要分别考虑以下几个方面,并实施不同的方案。

1. 显式设置线程亲和性

许多库的内部操作涉及多线程,即使主进程被限制到特定核心,子线程如果没有被同步处理亲和性,它们还是可以跑在任何核心。我们需要直接修改线程的 CPU 亲和性。 Python threading 模块配合 os 模块的 sched_setaffinity 方法可以实现。以下代码展示如何创建一个受限制核心影响的线程:

import threading
import os
import sched
import time

def worker(message):
    """模拟一个执行任务的线程函数"""
    print(f"Thread {threading.get_ident()} started on CPU core: {os.sched_getaffinity(0)}")
    time.sleep(1)  # 模拟工作
    print(f"Thread {threading.get_ident()} finish:{message}")

def create_restricted_thread(cpu_core, task_name):
  """创建一个限制在特定CPU核心上的线程"""
  def thread_wrapper(func,args=(), kwargs={}):
    def wrapped_func():
        #获取当前线程的pid
        pid = os.getpid()
        # 设置线程亲和性到指定核心
        os.sched_setaffinity(pid, [cpu_core])
        # 在线程内部运行目标任务
        func(*args,**kwargs)
    return wrapped_func
  # 生成实际的线程函数
  task = thread_wrapper(worker,(task_name,))
  t = threading.Thread(target=task)
  t.start()
  return t
  
if __name__ == "__main__":
  
  create_restricted_thread(2,"task_2") #任务2 使用cpu核心 2
  create_restricted_thread(3,"task_3") #任务3 使用cpu核心 3
  print("All task submitted.")
  • 操作步骤:
    1. 引入必要的模块 threading, os, time.
    2. worker() 函数模拟了一个耗时任务,同时会打印线程所在CPU核心信息。
    3. create_restricted_thread() 函数中首先创建 thread_wrapper, 利用闭包方式传递函数引用和入参信息。 在内部设置了 CPU 亲和性 os.sched_setaffinity(pid,[cpu_core])。确保新的线程在指定的CPU核心上运行。
    4. 通过 create_restricted_thread() 传入目标函数和核心id, 生成对应线程并开始执行。

此方法能够有效控制 threading 创建的线程,但无法影响库自身创建的线程或进程。 考虑到并非所有库都暴露了内部线程创建方式,此方法对解决 Piper TTS 场景可能不直接。

2. 使用 cgroups 进行资源控制

Linux Control Groups (cgroups) 是一种强大的内核特性,它可以限制、监控和管理进程组的资源使用,包括 CPU,内存等。通过 cgroups, 我们能够更精细地控制 Piper TTS 的资源使用,甚至可以限制库内部使用的线程或进程的CPU使用。

以下命令可以创建和配置一个 cgroup,并限制其只能使用特定的CPU核心。

# 创建一个新的cgroup
sudo cgcreate -g cpu:my_piper_tts

# 将当前shell进程ID添加到该cgroup
sudo cgclassify -g cpu:my_piper_tts $
echo $

# 设置cgroup的CPU亲和性为 2,3 号核心(示例,核心ID可以根据需求更改)
echo "2-3" | sudo tee /sys/fs/cgroup/cpu/my_piper_tts/cpuset.cpus

#  在cgroup中运行piper的程序 (假设 piper 程序为`python myprogram.py` )
sudo cgexec -g cpu:my_piper_tts python myprogram.py

# 查看cgroup状态,可验证是否正常运行在指定CPU上
cat /sys/fs/cgroup/cpu/my_piper_tts/cpu.stat
  • 操作步骤:
    1. 使用 cgcreate 创建一个新的名为 my_piper_ttscgroup, 类型为 cpu
    2. 通过 cgclassify 将当前 shell 进程加入这个 cgroup$ $ 指的是当前 shell 的进程 ID。
    3. 使用 echotee 命令设置 cgroup 的 CPU 亲和性,例如,"2-3" 表示使用 23 号核心。 请注意,此处的ID是系统可识别的 CPU核心逻辑ID。
    4. 使用cgexec 命令在新创建的cgroup中启动 Python 脚本。所有此 cgroup 下的进程及其子进程都会被应用之前设置好的 CPU 限制。
  1. 查看 cpu.stat文件确认状态。

这个方法的好处是可以覆盖程序本身启动的子进程/线程的CPU占用,具有更广泛的应用场景。 cgroups 是一个相对高级的工具,但能够提供非常细致的资源控制。
如果系统使用systemd,还可以直接通过服务配置中的CPUAffinity实现同样效果。

额外提示

  • 运行涉及到系统资源管理的命令时,务必使用sudo或具有足够权限的用户。
  • 如果需要长期应用此配置,可以将相关 cgroup 的设置持久化,以防止重启丢失。具体方法需要查询系统 cgroups 的文档。
  • 请务必了解操作系统的相关 CPU 核心配置,设置时要准确对应可用的逻辑核心 ID,防止配置错误。
  • 测试时要关注应用内部的工作原理和并发行为,确保限制的有效性。

总而言之,虽然 tasksetpsutil 在设置 Python 进程 CPU 亲和性方面十分便利,当涉及到复杂的库(如 Piper TTS )内部使用了多个线程,进程甚至更复杂结构时, 更加细粒度的控制需要借助于操作系统提供的更底层的机制(如 cgroups) 。 cgroups 不仅能解决本文的问题,它还是操作系统资源管理和分配的基础设施。