news 2026/9/11 15:58:06

KvikIO GPU 文件 I/O 实战指南:用 GPUDirect Storage 打通存储与显存

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
KvikIO GPU 文件 I/O 实战指南:用 GPUDirect Storage 打通存储与显存

KvikIO GPU 文件 I/O 实战指南:用 GPUDirect Storage 打通存储与显存

【免费下载链接】scientific-agent-skillsTurn any AI agent into an AI Scientist. The #1 Agent Skills library for science, used by 190,000+ scientists worldwide. 165 ready-to-use validated skills plus 100+ scientific databases covering biology, chemistry, medicine, and drug discovery. Compatible with Cursor, Claude Code, Codex, Pi, Antigravity, and the open Agent Skills standard.项目地址: https://gitcode.com/GitHub_Trending/cl/scientific-agent-skills

本文是 scientific-agent-skills 仓库中 optimize-for-gpu 技能下关于 KvikIO 的完整技术指南。KvikIO 是 RAPIDS 生态中的高性能文件 I/O 库,通过封装 NVIDIA cuFile 提供 GPUDirect Storage(GDS)能力,让数据在存储与显存之间直接流动、彻底绕开 CPU 内存中转;当 GDS 不可用时又能无缝回退到 POSIX I/O。读完本文,你将掌握:如何安装与验证 KvikIO、用CuFile读写本地文件、用RemoteFile从 S3/HTTP/WebHDFS 直接读入 GPU、在 Zarr 上使用GDSStore、按需调优线程池与任务大小等运行时参数,以及如何用非阻塞 I/O 实现数据加载与 GPU 计算的流水线重叠。

什么是 KvikIO:为 GPU 工作负载而生的文件 I/O 层

KvikIO 是一个同时提供 Python 与 C++ 接口的高性能文件 I/O 库,核心价值来自它对 NVIDIA cuFile 的绑定:

  • GPUDirect Storage(GDS):启用后,读写操作在 NVMe 存储与 GPU 显存之间直接传输,完全绕开 CPU 内存(即不再经过"磁盘 → 主机内存 → 显存"的两次拷贝路径);
  • 优雅回退:当 GDS 不可用(例如缺少libcufile.so、运行在 WSL、Docker 未挂载/run/udev)时,自动回退到 POSIX I/O,同时照常支持主机内存与设备内存两类缓冲;
  • 生态互操作:KvikIO 隶属于 RAPIDS 生态,与 CuPy、cuDF、Numba 及其他 GPU 库通过 CUDA Array Interface 零拷贝互通。

在 decision_framework.md 的选型矩阵中,KvikIO 被定位为"原始/远程文件 I/O"场景的首选路径:当工作负载是"把大二进制文件直接加载进显存""把 GPU 数组直接写盘""从 S3/HTTP/WebHDFS 读入显存""在 GPU 上使用 Zarr 分块数组"或"文件 I/O 成为存储与 GPU 之间的瓶颈"时使用 KvikIO。而 SKILL.md 同时强调:对 CSV、Parquet、JSON 这类表格化格式,应改用 cuDF 自带的高性能读取器,KvikIO 只负责原始二进制数据与远程文件访问。

安装与验证

KvikIO 属于 RAPIDS 26.06 系列包,发布为-cu12-cu13两种 wheel 变体,需要根据本机 CUDA 版本选择。仓库约定使用uv add,且 RAPIDS 包需要 NVIDIA 的 extra index(因为部分包的可用性差异,许多包也同时镜像在 PyPI 上):

# CUDA 12.x uv add "kvikio-cu12==26.6.*" # CUDA 13.x uv add "kvikio-cu13==26.6.*" # 可选:Zarr 支持(KvikIO 26.06 的 GPU store 后端依赖 Zarr 3.x) uv add "zarr==3.*"

对应 installation.md 中的完整写法,是显式带上 NVIDIA 索引:

uv add --extra-index-url=https://pypi.nvidia.com "kvikio-cu12==26.6.*" uv add "zarr==3.*" # 可选,用于 Zarr GPU 后端

安装后验证 GDS 是否可用:

import kvikio import kvikio.cufile_driver # True 表示 GDS 已就绪;False 表示将回退到 POSIX I/O print(kvikio.cufile_driver.get("is_gds_available"))

需要注意的运行前提(SKILL.md 中的兼容性声明):GPU 执行要求 NVIDIA CUDA 显卡;RAPIDS 26.06 要求 Python 3.11+ 且运行于 Linux 或 WSL2,并匹配 CUDA 12 或 13 的 wheel。

什么时候该用(以及不该用)KvikIO

适合使用 KvikIO 的场景:

  • 把大型二进制数据直接加载到显存——省去标准open()或 NumPyfromfile()所必需的 CPU 内存拷贝;
  • 把 GPU 数组直接写盘——从设备内存直接落盘,无需先拷回主机;
  • 从远程存储(S3、HTTP、WebHDFS)读入显存——跳过主机内存中转;
  • 在 GPU 上处理 Zarr 数组——GDSStore后端直接把 chunk 读成 CuPy 数组;
  • I/O 成为瓶颈——GDS 可逼近裸 NVMe 带宽(每块盘约 6–7 GB/s),而标准 I/O 受限于 CPU 内存带宽;
  • 需要 I/O 与计算重叠——非阻塞读写让你把数据加载与 GPU 计算流水化。

KvikIO 不合适的场景:

  • 数据很小(< 1 MB)——内核启动与 GDS 开销占主导,得不偿失;
  • 读取结构化格式(CSV、Parquet、JSON)——应使用 cuDF 自带的高性能读取器(cudf.read_csv()cudf.read_parquet()等);
  • 只需要主机内存——标准 Python I/O 更简单。

CuFile:本地文件 I/O

kvikio.CuFile是本地文件 I/O 的主接口,面向 GPU 工作负载时替代 Python 的open()

基本用法

import cupy as cp import kvikio # 把 GPU 数组写入磁盘 a = cp.arange(1_000_000, dtype=cp.float32) with kvikio.CuFile("data.bin", "w") as f: f.write(a) # 再读回来 b = cp.empty(1_000_000, dtype=cp.float32) with kvikio.CuFile("data.bin", "r") as f: f.read(b) assert cp.all(a == b)

API 方法一览

方法阻塞说明
read(buf, size, file_offset)读入设备或主机缓冲
write(buf, size, file_offset)从设备或主机缓冲写出
pread(buf, size, file_offset)非阻塞并行读,返回IOFuture
pwrite(buf, size, file_offset)非阻塞并行写,返回IOFuture
raw_read(buf, size, file_offset)底层单线程读(仅设备)
raw_write(buf, size, file_offset)底层单线程写(仅设备)
raw_read_async(buf, stream, size, file_offset)基于 CUDA 流的异步读(仅设备)
raw_write_async(buf, stream, size, file_offset)基于 CUDA 流的异步写(仅设备)

文件模式与open()一致:"r"(读)、"w"(写/截断)、"a"(追加)、"+"(读写)。

用 Future 实现非阻塞 I/O

pread/pwrite会把操作拆分为多个任务交给线程池执行,并返回IOFuture,让你在 I/O 进行期间穿插其他工作:

import cupy as cp import kvikio data = cp.empty(10_000_000, dtype=cp.float32) with kvikio.CuFile("data.bin", "r") as f: # 为不同区段发起两个非阻塞读 future1 = f.pread(data[:5_000_000]) future2 = f.pread(data[5_000_000:], file_offset=5_000_000 * 4) # I/O 进行期间可以做其他事... # 等待完成 bytes_read1 = future1.get() bytes_read2 = future2.get()

部分读取与写入

import cupy as cp import kvikio # 只读文件的一部分 buf = cp.empty(1000, dtype=cp.float32) with kvikio.CuFile("data.bin", "r") as f: # 从字节偏移 4000 处开始读 4000 字节(1000 个 float32) f.read(buf, size=4000, file_offset=4000)

主机内存支持

KvikIO 对主机内存的读写是透明的,无需任何特殊 API:

import numpy as np import kvikio # 从主机内存写出 a = np.arange(1_000_000, dtype=np.float32) with kvikio.CuFile("data.bin", "w") as f: f.write(a) # 读入主机内存 b = np.empty_like(a) with kvikio.CuFile("data.bin", "r") as f: f.read(b)

GDS 对齐规则

GDS 在页对齐 I/O 下表现最佳。GPU 页大小为 4 KiB(4096 字节):

  • 文件偏移:应为 4096 的倍数;
  • 传输大小:应为 4096 的倍数。

KvikIO 能正确处理未对齐的 I/O,但会将其拆分为对齐与未对齐两部分分别处理,因此对齐 I/O 会明显更快。从源码结构看(见下文"性能优化"小节),这也是性能调优的首要检查项。

RemoteFile:S3、HTTP 与 WebHDFS

kvikio.RemoteFile可直接把远程文件读入 GPU 或主机内存。

HTTP/HTTPS

import cupy as cp import kvikio buf = cp.empty(1_000_000, dtype=cp.float32) with kvikio.RemoteFile.open_http("https://example.com/data.bin") as f: print(f.nbytes()) # 文件大小 f.read(buf)

AWS S3

import cupy as cp import kvikio # 使用桶名 + 对象名(需要 AWS 环境变量或显式凭据) with kvikio.RemoteFile.open_s3("my-bucket", "data/file.bin") as f: buf = cp.empty(f.nbytes(), dtype=cp.uint8) f.read(buf) # 使用 S3 URL with kvikio.RemoteFile.open_s3_url("s3://my-bucket/data/file.bin") as f: buf = cp.empty(f.nbytes(), dtype=cp.uint8) f.read(buf) # 公有 S3(无需凭据) with kvikio.RemoteFile.open_s3_public("s3://public-bucket/data.bin") as f: buf = cp.empty(f.nbytes(), dtype=cp.uint8) f.read(buf) # 预签名 URL with kvikio.RemoteFile.open_s3_presigned_url(presigned_url) as f: buf = cp.empty(f.nbytes(), dtype=cp.uint8) f.read(buf)

AWS 凭据来自环境变量(AWS_DEFAULT_REGIONAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY),也可作为关键字参数显式传入。

自动识别端点类型

import kvikio # KvikIO 会根据 URL 自动识别协议 with kvikio.RemoteFile.open("s3://bucket/object") as f: ... with kvikio.RemoteFile.open("https://example.com/file.bin") as f: ...

WebHDFS

import kvikio with kvikio.RemoteFile.open_webhdfs("http://namenode:9870/path/to/file") as f: buf = cp.empty(f.nbytes(), dtype=cp.uint8) f.read(buf)

RemoteFile 读入主机内存

RemoteFile 同样可以轻松读入主机内存:

import numpy as np import kvikio with kvikio.RemoteFile.open_http("https://example.com/data.bin") as f: buf = np.empty(f.nbytes(), dtype=np.uint8) f.read(buf)

Zarr 集成:GDSStore

KvikIO 为 Zarr(3.x)提供了 GPU store 后端GDSStore,可以把分块的 N 维数组通过 GDS 直接读写到 GPU 显存:

import zarr from kvikio.zarr import GDSStore # 在 Zarr 中启用 GPU 支持 zarr.config.enable_gpu() # 创建 GDS 后端 store store = GDSStore(root="data.zarr") # 创建并写入 Zarr 数组(数据始终留在 GPU 上) z = zarr.create_array( store=store, shape=(1000, 1000), chunks=(100, 100), dtype="float32", overwrite=True, ) # 读取返回 CuPy 数组 chunk = z[:100, :100] # 返回 cupy.ndarray

Zarr + KvikIO 的典型应用场景:

  • 气候/气象数据(大型多维数组);
  • 生物信息学(基因组数组);
  • 任何需要 GPU 处理的分块数组工作负载。

依赖要求:在 KvikIO 26.06 基础上额外安装zarr==3.*(见安装小节)。在 decision_framework.md 的组合建议中,KvikIO + Zarr是明确推荐的组合之一:用 GDSStore 后端在 GPU 上直接读写分块 N 维数组。

内存映射文件

kvikio.mmap.Mmap提供内存映射文件访问,同时支持主机与设备两种目的地:

from kvikio.mmap import Mmap import cupy as cp # 映射文件用于读取 with Mmap("data.bin", flags="r") as m: print(m.file_size()) # 顺序读入设备内存 buf = cp.empty(1000, dtype=cp.float32) m.read(buf, size=4000, offset=0) # 并行读(返回 IOFuture) future = m.pread(buf, size=4000, offset=0) future.get()

运行时设置

KvikIO 的行为通过环境变量或kvikio.defaultsAPI 控制。

关键设置项

设置项环境变量默认值说明
兼容模式KVIKIO_COMPAT_MODEAUTOON:仅 POSIX;OFF:仅 GDS;AUTO:先试 GDS,失败回退
线程池大小KVIKIO_NTHREADS1pread/pwrite使用的 I/O 线程数
任务大小KVIKIO_TASK_SIZE4 MiB单个并行 I/O 任务的最大字节数
GDS 阈值KVIKIO_GDS_THRESHOLD16 KiB使用 GDS 的最小字节数(更小则走 POSIX)
反弹缓冲大小KVIKIO_BOUNCE_BUFFER_SIZE16 MiB每个线程的中间主机缓冲大小
直接 I/O 读KVIKIO_AUTO_DIRECT_IO_READoff读操作的机会性O_DIRECT
直接 I/O 写KVIKIO_AUTO_DIRECT_IO_WRITEon写操作的机会性O_DIRECT

编程式配置

import kvikio.defaults # 查询设置 print(kvikio.defaults.get("compat_mode")) print(kvikio.defaults.get("num_threads")) # 运行时修改设置 kvikio.defaults.set({"num_threads": 16, "task_size": 8 * 1024 * 1024}) # 启用读操作的直接 I/O kvikio.defaults.set({"auto_direct_io_read": True})

兼容模式详解

当 GDS 不可用(缺少libcufile.so、运行于 WSL、Docker 未挂载/run/udev)时,AUTO模式会自动回退到 POSIX I/O。这意味着用 KvikIO 写的代码在任何地方都能跑——GDS 可用时只是跑得更快。

import kvikio.cufile_driver # 确认 GDS 是否真正生效 print(kvikio.cufile_driver.get("is_gds_available"))

cuFile 驱动配置

import kvikio.cufile_driver # 查询驱动属性 print(kvikio.cufile_driver.get("is_gds_available")) print(kvikio.cufile_driver.get("major_version")) # 配置可设置属性 kvikio.cufile_driver.set("max_device_cache_size", 1024) # 作为上下文管理器使用(退出时自动还原) with kvikio.cufile_driver.set({"poll_mode": True}): # 此处 poll mode 生效 ... # 退出后 poll mode 自动还原

性能优化

结合 SKILL.md 的优化工作流(先建立基线与契约、确认 GPU 适配性、优先最小改动方案、验证语义、正确计时),KvikIO 自身的调优点如下。

1. 提高线程池大小

默认 1 个线程偏保守。对大型文件应调大:

import kvikio.defaults kvikio.defaults.set({"num_threads": 16})

pread/pwrite会把单次操作拆分成多个任务交给该线程池执行,因此线程数直接影响大文件的并行吞吐。同时可配合KVIKIO_TASK_SIZE控制每个任务的粒度。

2. 用非阻塞 I/O 实现流水线

使用pread/pwrite让 I/O 与计算重叠:

import cupy as cp import kvikio # 流水线:读第 N 个 chunk 时处理第 N-1 个 chunk chunk_size = 10_000_000 buf_a = cp.empty(chunk_size, dtype=cp.float32) buf_b = cp.empty(chunk_size, dtype=cp.float32) with kvikio.CuFile("large_data.bin", "r") as f: # 发起第一次读 future = f.pread(buf_a) future.get() for offset in range(chunk_size * 4, file_size, chunk_size * 4): # 处理当前块的同时发起下一次读 next_future = f.pread(buf_b, file_offset=offset) # 在 GPU 上处理 buf_a(与 I/O 重叠) result = cp.fft.fft(buf_a) next_future.get() buf_a, buf_b = buf_b, buf_a # 交换缓冲

3. 对齐到页边界

GDS 在 4 KiB 对齐的偏移与大小下性能最佳:

# 好:偏移与大小均对齐 f.read(buf, size=4096 * 1000, file_offset=4096 * 10) # 较慢:未对齐(KvikIO 能处理,但会拆成对齐 + 未对齐两部分) f.read(buf, size=5000, file_offset=100)

4. 启用直接 I/O

对顺序写与冷读,直接 I/O(绕过操作系统页缓存)可能有帮助:

import kvikio.defaults kvikio.defaults.set({ "auto_direct_io_read": True, "auto_direct_io_write": True, })

注意默认值是"读关、写开"(见运行时设置表),开启前请确认文件系统与文件访问模式兼容O_DIRECT的对齐要求。

5. 调节任务大小与反弹缓冲

对非常大的文件,增大任务与反弹缓冲大小:

import kvikio.defaults kvikio.defaults.set({ "task_size": 16 * 1024 * 1024, # 每个任务 16 MiB "bounce_buffer_size": 64 * 1024 * 1024, # 反弹缓冲 64 MiB })

反弹缓冲是 GDS 路径上每个线程的中间主机缓冲:当 I/O 无法对齐或需要兼容处理时,数据经由它中转,因此其大小会影响大块传输的吞吐上限。

6. 页缓存工具

基准测试时,清空页缓存可以测量冷读性能:

import kvikio # 检查页缓存驻留情况 pages_cached, total_pages = kvikio.get_page_cache_info("data.bin") print(f"{pages_cached}/{total_pages} pages in cache") # 丢弃单个文件的页缓存(无需提权;26.04 起提供) kvikio.drop_file_page_cache("data.bin") # 丢弃系统级页缓存(需要提升权限) kvikio.drop_system_page_cache()

kvikio.clear_page_cache()自 26.04 起已弃用——请改用drop_system_page_cache()(或按文件的drop_file_page_cache())。

与其他库的互操作

KvikIO 支持任何实现了 CUDA Array Interface 的缓冲,因此与整个 RAPIDS 生态零拷贝互通。

与 CuPy 配合

读入 CuPy 数组是最常见用法:

import cupy as cp import kvikio data = cp.empty(1_000_000, dtype=cp.float64) with kvikio.CuFile("data.bin", "r") as f: f.read(data) # data 现在是一个 CuPy 数组,可直接用于 GPU 计算

与 Numba CUDA 配合

from numba import cuda import kvikio d_arr = cuda.device_array(1_000_000, dtype="float32") with kvikio.CuFile("data.bin", "r") as f: f.read(d_arr)

与 cuDF 配合

对非表格化的原始二进制数据,先用 KvikIO 加载,再转换为 cuDF:

import cupy as cp import cudf import kvikio # 加载原始 float 数组,包装为 cuDF Series buf = cp.empty(1_000_000, dtype=cp.float32) with kvikio.CuFile("signal.bin", "r") as f: f.read(buf) signal = cudf.Series(buf)

对于表格化格式(CSV、Parquet、JSON、ORC),请使用 cuDF 自带的读取器——它们针对这些格式做了专门优化,这与 decision_framework.md 中的明确提示一致。

与 NumPy 配合(主机内存)

import numpy as np import kvikio arr = np.empty(1_000_000, dtype=np.float32) with kvikio.CuFile("data.bin", "r") as f: f.read(arr)

在 decision_framework.md 的组合矩阵中,KvikIO + CuPy(经 GDS 绕开 CPU 内存把原始二进制直接读入 CuPy 数组)与KvikIO + Numba(读入 GPU 后用自定义 Numba CUDA 内核处理)是官方推荐的组合模式。

常见模式

保存与加载 GPU 模型检查点

import cupy as cp import kvikio def save_checkpoint(arrays: dict[str, cp.ndarray], path: str): """把多个 GPU 数组保存到单个文件。""" with kvikio.CuFile(path, "w") as f: offset = 0 for arr in arrays.values(): f.write(arr, file_offset=offset) offset += arr.nbytes def load_checkpoint(shapes_dtypes: dict, path: str) -> dict[str, cp.ndarray]: """从检查点文件加载 GPU 数组。""" arrays = {} with kvikio.CuFile(path, "r") as f: offset = 0 for name, (shape, dtype) in shapes_dtypes.items(): arr = cp.empty(shape, dtype=dtype) f.read(arr, file_offset=offset) offset += arr.nbytes arrays[name] = arr return arrays

从 S3 流式读入 GPU 并处理

import cupy as cp import kvikio with kvikio.RemoteFile.open_s3("my-bucket", "large-dataset.bin") as f: total_bytes = f.nbytes() chunk_size = 100 * 1024 * 1024 # 100 MB 块 buf = cp.empty(chunk_size // 4, dtype=cp.float32) for offset in range(0, total_bytes, chunk_size): size = min(chunk_size, total_bytes - offset) f.read(buf[:size // 4], size=size, file_offset=offset) # 在 GPU 上处理当前块 result = cp.mean(buf[:size // 4])

用 KvikIO 替换 GPU 工作负载中的open()

# 之前:CPU 侧文件 I/O,两次拷贝 import numpy as np data = np.fromfile("data.bin", dtype=np.float32) import cupy as cp gpu_data = cp.asarray(data) # 额外拷贝:磁盘 → CPU → GPU # 之后:直接到 GPU import cupy as cp import kvikio gpu_data = cp.empty(1_000_000, dtype=cp.float32) with kvikio.CuFile("data.bin", "r") as f: f.read(gpu_data) # 磁盘 → GPU 直连(启用 GDS 时)

这个"Before/After"转换同样记录在 code_transformation_patterns.md 的"File IO to GPU with KvikIO"一节中,包含本地文件与 S3 URL 两个版本:前者把numpy.fromfile()+cp.asarray()的两次拷贝路径替换为一次 GDS 直读;后者用RemoteFile.open_s3_url直接把 S3 对象读进 CuPy 数组。这也是该技能推荐的标准化改造模板。

常见陷阱

  1. 忘记设置线程池大小——默认只有 1 个线程。对大型文件,kvikio.defaults.set({"num_threads": 16})可显著提升吞吐。
  2. 用 KvikIO 读结构化格式——不要用 KvikIO 读 CSV/Parquet/JSON,应使用cudf.read_csv()cudf.read_parquet()等;KvikIO 只负责原始二进制数据。
  3. 不检查 GDS 可用性——没有 GDS 时代码也能正常跑(回退到 POSIX),但拿不到完整带宽收益。用kvikio.cufile_driver.get("is_gds_available")确认。
  4. 性能关键路径上未对齐 I/O——偏移与大小应使用 4 KiB 对齐,以获得最佳 GDS 性能。
  5. 不使用上下文管理器——始终用with kvikio.CuFile(...),确保文件被正确关闭与注销(deregister)。
  6. 期望 RemoteFile 支持写入——RemoteFile是只读的。要写远程存储,先写本地,再用相应 SDK(如 S3 用 boto3)上传。
  7. Docker 中未配置 GDS——在 Docker 中以只读方式挂载/run/udev--volume /run/udev:/run/udev:ro),否则 KvikIO 会静默回退到 POSIX。

小结

KvikIO 的价值在于把"存储 ↔ 显存"路径上的 CPU 中转彻底移除:本地用CuFile、远程用RemoteFile、分块数组用GDSStore,并通过kvikio.defaultskvikio.cufile_driver完成运行时调优。配合 optimize-for-gpu 技能的整体方法(先测量基线、再选最小改动方案、最后做同步基准验证),KvikIO 是"原始/远程文件 I/O"GPU 化改造的标准答案。需要进一步了解它在整个 GPU 优化工具箱中的定位,可继续阅读 decision_framework.md(选型决策)与 installation.md(完整安装指引)。

【免费下载链接】scientific-agent-skillsTurn any AI agent into an AI Scientist. The #1 Agent Skills library for science, used by 190,000+ scientists worldwide. 165 ready-to-use validated skills plus 100+ scientific databases covering biology, chemistry, medicine, and drug discovery. Compatible with Cursor, Claude Code, Codex, Pi, Antigravity, and the open Agent Skills standard.项目地址: https://gitcode.com/GitHub_Trending/cl/scientific-agent-skills

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 15:57:52

嵌入式软硬件协作中的“互相等”困局与破局方法

我参与过的嵌入式项目&#xff0c;几乎没有哪个没有经历过这个场景&#xff1a;硬件工程师在群里说“原理图已经定稿&#xff0c;等软件把IO分配表确认一下”&#xff0c;软件工程师在另一个群回“驱动我写好了&#xff0c;等硬件板子回来就调”。然后两边各忙各的&#xff0c;…

作者头像 李华
网站建设 2026/9/11 15:53:27

Flutter for OpenHarmony 实战:从环境配置到轮播组件深度定制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华