python / python/cpython

Docs: `set_forkserver_preload()` does not cover lazily imported modules

オープン
#157,216 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

docs topic-multiprocessing
主要言語
Python
スター
77.2k
フォーク
35.9k
PR マージ指標
PR 指標を取得中

説明

Title: Docs: set_forkserver_preload() does not cover lazily imported modules

Category: Documentation (misleading documentation)

Page: https://docs.python.org/3/library/multiprocessing.html#multiprocessing.set_forkserver_preload

Source: Doc/library/multiprocessing.rst

Current text

Set a list of module names for the forkserver main process to attempt to import so that their already imported state is inherited by forked processes. Any ImportError when doing so is silently ignored. This can be used as a performance enhancement to avoid repeated work in every process.

"their already imported state is inherited" is accurate for each named module, but does not say whether that module's dependencies are inherited too. Where a dependency is imported inside a function body rather than at module level, it is not, and it is imported again in every child.

Demonstration

Two stdlib examples, both reached through ordinary public calls, where the import happens inside the callee:

  • datetime.datetime.strptime() imports _strptime on first call. That pulls in 12 modules: _strptime, re (with re._casefix, re._compiler, re._constants, re._parser), locale, _locale, calendar, enum, copyreg and _sre.

  • Reading importlib.metadata.Distribution.metadata imports importlib.metadata._adapters and ._text. That pulls in 26 modules, mostly the email package (email.message, email.parser, email.feedparser, email.charset, email.header, email.utils and others), plus base64, quopri, urllib.parse, ipaddress, string, math and datetime.

Neither set is reachable by naming a module in the preload list, because neither import happens at module level.

Measured on 3.14.7, Linux x86-64, fresh interpreter per case, 9 runs each:

modules added private memory
import datetime 2 228 kB
first datetime.strptime() call 12 3872 kB
import importlib.metadata 75 6948 kB
first Distribution.metadata read 26 2220 kB

Module counts are exact and reproduced identically on every run. The memory figures are arena-granular, so expect a few hundred kB of variation between environments; the metadata figure also depends on the size of the first distribution's METADATA file (64 KB in this environment). So naming both parent modules in the preload list still leaves 38 modules and roughly 5.9 MiB to be allocated privately in each child. The forkserver process is minimal by design, so these transitive imports are not already satisfied there. In an application running 100 children this is about 600MiB.

Reproducer

"""set_forkserver_preload() does not cover lazily imported dependencies.

$ python3 forkserver_lazy_preload.py           # modules preloaded by name
$ WARM=1 python3 forkserver_lazy_preload.py    # ... plus one call to each
"""

from __future__ import annotations

import datetime
import importlib.metadata
import multiprocessing as mp
import os
import sys

# Imported inside datetime.strptime() and Distribution.metadata respectively,
# so naming their parent modules in the preload list does not pull them in.
LAZY = ("_strptime", "importlib.metadata._adapters")
PAGE_KB = os.sysconf("SC_PAGE_SIZE") // 1024


def touch():
    """Two ordinary calls whose imports happen inside the callee."""
    datetime.datetime.strptime("2026-01-01", "%Y-%m-%d")
    next(iter(importlib.metadata.distributions())).metadata["Name"]


# Runs in the forkserver as well, because "__main__" is in the preload list.
if int(os.environ.get("WARM", "0")) == 1:
    touch()


def rss_kb():
    with open("/proc/self/statm") as statm:
        return int(statm.read().split()[1]) * PAGE_KB


def child(queue):
    inherited = sum(name in sys.modules for name in LAZY)
    before = rss_kb()
    touch()
    queue.put((inherited, rss_kb() - before))


if __name__ == "__main__":
    mp.set_start_method("forkserver")
    mp.set_forkserver_preload(["__main__", "datetime", "importlib.metadata"])

    queue = mp.Queue()
    children = [mp.Process(target=child, args=(queue,)) for _ in range(10)]
    for proc in children:
        proc.start()
    rows = [queue.get() for _ in children]
    for proc in children:
        proc.join()

    print(f"python {sys.version.split()[0]}  WARM={os.environ.get('WARM', '0')}")
    print(f"  lazy deps present in child : {sum(r[0] for r in rows)}/{2 * len(rows)}")
    print(f"  private memory per child   : {sum(r[1] for r in rows) // len(rows)} kB")

datetime and importlib.metadata are named in the preload list explicitly, to show that naming them is not sufficient. WARM=1 additionally calls each function at module level, which — because "__main__" is preloaded — runs in the forkserver process.

❯ WARM=0 python3.14 forkserver_lazy_preload.py
  lazy deps present in child : 0/20
  private memory per child   : 1852 kB


❯ WARM=1 python3.14 forkserver_lazy_preload.py
  lazy deps present in child : 20/20
  private memory per child   : 576 kB

Proposed addition

Appended to the set_forkserver_preload() entry:

 Only the modules named in *module_names* are imported. If one of them imports a further
 module from inside a function body rather than at module level, that further module is
 not imported in the forkserver process, and is imported again in each child. For
 example, listing ``"datetime"`` imports :mod:`datetime`, while :mod:`!_strptime` and
 the :mod:`re`, :mod:`locale` and :mod:`calendar` modules it imports are loaded by the
 first call to :meth:`~datetime.datetime.strptime` in each child process. To inherit
 those as well, call such a function at the module level of a preloaded module, so that
 the import happens in the forkserver process.

Related issues

gh-117378, gh-98552 and gh-141860 concern preload imports that fail silently (ImportError swallowed, sys_path ignored, main_path renamed). This report concerns preload imports that succeed exactly as documented, which no error reporting would surface.

Environment

Python 3.14.7, Linux x86-64.

Linked PRs
  • gh-157229
  • gh-157237

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

Doc/library/multiprocessing.rst の set_forkserver_preload() エントリを編集してください。この issue における現在の文言と提案された追加内容を読み、関数本体内の import は継承されないこと、およびそれらを事前ロードする方法を説明するようにドキュメントを更新してください。ドキュメントは datetime の例と、示されている動作を正確に反映する必要があります。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
python
領域
documentation
issue の種類
ドキュメント
難易度
1/5
見積もり時間
1〜3時間
活発さ
停滞
明瞭さ
明確に書かれている
初心者へのやさしさ
25/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。