网易首页 > 网易号 > 正文 申请入驻

现代CUDA工具链实战:六步优化GPU图像处理流水线

0
分享至


NVIDIA CUDA是GPU加速计算的基石,支撑着从科学仿真到大规模AI训练的各类应用。

然而,编写正确、可维护且高性能的CUDA代码并非易事:内存错误往往隐蔽难查,缺乏合适工具时性能瓶颈几乎无从发现,手写GPU算法的效率也很难媲美经过优化的库。好消息是,现代CUDA工具链已日趋成熟,上述难题大多都有了简明的解决方案。

本文将介绍NVIDIA提供的一套调试、基准测试与性能优化工具,并通过对示例代码的逐步改造,演示如何仅凭少量代码改动,就让程序变得更安全、更易维护、更快速。

全文分六个递进步骤,涵盖以下内容:

如何通过采用现代CCCL API与Compute Sanitizer快速定位索引越界错误

如何借助NVTX提升Nsight Systems的基准测试可读性

如何在块级与设备级使用CUB的优化算法

如何通过内存池容器管理GPU内存

如何借助固定内存容器加速主机到设备的数据传输

如何为每个线程分配独立流并使用异步传输来并行化GPU任务

本文还配套提供了完整代码,并支持在Google Colab上直接运行。

起点:图像处理流水线示例

示例任务如下:从输入的红、绿、蓝三通道图像流开始,先将数据从CPU传输至GPU,随后将RGB图像转换为灰度图。接着,对图像中每个32×32像素的分块,通过排序像素并取中间值来计算中值。最后,将每个分块的中值结果从GPU复制回CPU。

基础代码示例

以下是完整的初始代码,后续每个步骤都在此基础上进行改进。

```cpp

#define CUDA_CHECK_ERROR(call) \\

do { \\

cudaError_t err = call; \\

if (err != cudaSuccess) { \\

std::exit(EXIT_FAILURE); \\

} while (0)

using pixel_t = uint8_t;

__global__ void computeRGBToGray(

const pixel_t* d_image_r, const pixel_t* d_image_g,

const pixel_t* d_image_b, pixel_t* d_image_gray, int width, int height)

const int x = threadIdx.x + blockIdx.x * blockdim.x;

const int y = threadIdx.y + blockIdx.y * blockDim.y;

const int i = x + y * width;

d_image_gray[i] = static_cast(

0.299f * d_image_r[i] + 0.587f * d_image_g[i] + 0.114f * d_image_b[i]);

template

__global__ void computeMedian(pixel_t *d_image_gray, pixel_t *d_median, int width, int height)

const int x = threadIdx.x + blockIdx.x * blockDim.x;

const int y = threadIdx.y + blockIdx.y * blockDim.y;

__shared__ pixel_t tile[TILE_WIDTH * TILE_WIDTH];

const int index = x + y * width;

tile[index] = d_image_gray[index];

__syncthreads();

if (threadIdx.x == 0 && threadIdx.y == 0) {

if (tile[i] > tile[j]) cuda::std::swap(tile[i], tile[j]);

const int medianIndex = (TILE_WIDTH * TILE_WIDTH) / 2;

d_median[blockIdx.x + blockIdx.y * gridDim.x] = tile[medianIndex];

int main() {

constexpr auto TILE_WIDTH = 32;

constexpr auto HISTO_SIZE = 256;

constexpr auto NB_TILE_X = 250;

constexpr auto NB_TILE_Y = NB_TILE_X;

constexpr auto IMAGE_LENGTH = TILE_WIDTH * NB_TILE_X;

constexpr auto IMAGE_SIZE = IMAGE_LENGTH * IMAGE_LENGTH;

constexpr auto NB_IMAGES = 3;

constexpr auto INIT_VALUE = 4;

std::vector> h_images_r(NB_IMAGES, std::vector(IMAGE_SIZE, 4));

std::vector> h_images_g(NB_IMAGES, std::vector(IMAGE_SIZE, 4));

std::vector> h_images_b(NB_IMAGES, std::vector(IMAGE_SIZE, 4));

std::vector> h_images_gray(NB_IMAGES, std::vector(IMAGE_SIZE, 0));

std::vector> h_medians(NB_IMAGES, std::vector(NB_TILE_X * NB_TILE_Y));

#pragma omp parallel for

pixel_t *d_image_r, *d_image_g, *d_image_b, *d_image_gray, *d_median;

CUDA_CHECK_ERROR(cudaMalloc(&d_image_r, IMAGE_SIZE * sizeof(pixel_t)));

CUDA_CHECK_ERROR(cudaMalloc(&d_image_g, IMAGE_SIZE * sizeof(pixel_t)));

CUDA_CHECK_ERROR(cudaMalloc(&d_image_b, IMAGE_SIZE * sizeof(pixel_t)));

CUDA_CHECK_ERROR(cudaMalloc(&d_image_gray, IMAGE_SIZE * sizeof(pixel_t)));

CUDA_CHECK_ERROR(cudaMalloc(&d_median, (NB_TILE_X * NB_TILE_Y) * sizeof(pixel_t)));

CUDA_CHECK_ERROR(cudaMemcpy(d_image_r, h_images_r[i].data(), IMAGE_SIZE * sizeof(pixel_t), cudaMemcpyHostToDevice));

CUDA_CHECK_ERROR(cudaMemcpy(d_image_g, h_images_g[i].data(), IMAGE_SIZE * sizeof(pixel_t), cudaMemcpyHostToDevice));

CUDA_CHECK_ERROR(cudaMemcpy(d_image_b, h_images_b[i].data(), IMAGE_SIZE * sizeof(pixel_t), cudaMemcpyHostToDevice));

dim3 blockSize(TILE_WIDTH, TILE_WIDTH);

dim3 gridSize(cuda::ceil_div(IMAGE_LENGTH, blockSize.x), cuda::ceil_div(IMAGE_LENGTH, blockSize.y));

CUDA_CHECK_ERROR(cudaGetLastError());

CUDA_CHECK_ERROR(cudaGetLastError());

CUDA_CHECK_ERROR(cudaMemcpy(h_medians[i].data(), d_median, (NB_TILE_X * NB_TILE_Y) * sizeof(pixel_t), cudaMemcpyDeviceToHost));

CUDA_CHECK_ERROR(cudaFree(d_image_r));

CUDA_CHECK_ERROR(cudaFree(d_image_g));

CUDA_CHECK_ERROR(cudaFree(d_image_b));

CUDA_CHECK_ERROR(cudaFree(d_image_gray));

CUDA_CHECK_ERROR(cudaFree(d_median));

return 0;

代码定义了两个核函数:

computeRGBToGray:读取红、绿、蓝三通道输入图像的像素值,将其转换后写入灰度输出图像。

computeMedian:计算灰度图中每个分块的中值。每个线程块先将分块数据从全局内存加载到共享内存,再由单个线程对数组进行排序,并将排序后中间位置的值(即中值)写入全局输出数组。

在主函数中,定义完示例所需的常量后,为各图像和中值结果分配CPU内存。随后借助OpenMP并行地对三张图像运行处理流水线:先在GPU上分配内存,再将数据从CPU传至GPU,依次启动RGB转灰度和中值计算两个核函数,最后将中值结果拷回CPU并释放GPU内存。

该代码存在若干缺陷,将在后续步骤中逐一修正。

第一步:Compute Sanitizer与CCCL API——轻松发现错误,编写更安全的代码

首先尝试运行代码:

code_steps$ ./build/0_base_error_example

CUDA error in 0_base_error_example.cu at line 105: an illegal memory access was encountered

代码虽然包含错误检查,但遇到"非法内存访问"这类报错时,应当使用compute-sanitizer进一步排查。

使用NVIDIA的功能正确性检测工具Compute Sanitizer,可以直接定位一处难以肉眼发现的错误:

$ compute-sanitizer ./build/0_base_error_example

========= COMPUTE-SANITIZER

========= Invalid __shared__ write of size 1 bytes

========= by thread (0,3,0) in block (20,0,0)

========= Access at 0x6440 is out of bounds

工具明确指出,第55行存在共享内存越界写入:

```cpp

tile[index] = d_image_gray[index];

这行代码使用了全局索引来访问共享内存,而共享内存是以线程块为单位分配的,因此索引方式有误。为了避免此类索引错误,CCCL引入了新的API,用于区分全局索引和块级索引。使用方式如下,首先通过新的`cuda::launch` API启动核函数:

```cpp

auto config = cuda::make_config(cuda::block_dims(...), cuda::grid_dims(...));

cuda::launch(stream, config, kernel_name, input);

然后在核函数内部使用新的索引API:

```cpp

template

__global__ void kernel_name(Configuration config, ...) {

const auto [x, y, z] = cuda::gpu_thread.index(cuda::grid, config);

const auto block_idx = cuda::gpu_thread.index(cuda::block, config);

即便不使用Compute Sanitizer或新API,也可以通过将裸指针替换为`cuda::std::span`或其多维变体`cuda::std::mdspan`来直接发现此类错误。这两者均为连续内存的非拥有视图,在调试模式下,越界访问会触发断言,比裸指针更安全。

核函数可更新为:

```cpp

template

using span_2d = cuda::std::mdspan>;

template

__global__ void computeMedian(..., span_2dd_image_gray, ...)

应用上述修改后运行,会得到如下输出:

$ ./build/1_span

libcudacxx/include/cuda/std/__mdspan/mdspan.h:436:

operator(): block: [16,0,0], thread: [0,30,0]

Assertion `mdspan: operator() out of bounds access` failed.

共享内存中的访问同样应使用`cuda::shared_memory_mdspan`加以保护:

```cpp

__shared__ pixel_t shared[TILE_WIDTH * TILE_WIDTH];

cuda::shared_memory_mdspan tile_2d(shared, TILE_WIDTH, TILE_WIDTH);

完成这些修改后,代码便可正常运行,不再出现错误。

结合新的启动API与索引机制、基于裸指针的span封装以及Compute Sanitizer,越界访问要么从根本上被避免,要么在第一时间被捕获。更多关于Compute Sanitizer的信息,请参阅NVIDIA官方文档。

第二步:Nsight Systems与NVTX——对代码进行准确的基准测试

代码修复完毕后,可以使用NVIDIA Nsight Systems进行基准测试。该工具能可视化程序的执行时间线,清晰呈现每个函数的调用时机与持续时长。

为了让时间线更易于阅读,可使用NVTX对关键代码段进行标注:

```cpp

void image_compute(...) {

nvtx3::scoped_range fun_scope("Image compute");

nvtxRangePushA("Kernel median");

// 启动计算中值的GPU核函数

nvtxRangePop();

分析结果显示:

在分析器输出的GPU硬件(CUDA HW)部分,GPU时间中98.5%用于核函数执行,仅1.5%用于内存操作。

两个核函数中,中值计算核函数占用了绝大部分运行时间,每张图像耗时约2.1秒(computeMedian的统计数据显示耗时2.142秒)。

从CPU(线程)部分来看,三张图像的全部计算共耗时6.8秒,其中大部分时间用于三张灰度图像的中值计算。

由此可以确定首要优化目标。更多关于Nsight Systems的信息请参阅NVIDIA官方文档,NVTX的详细用法亦可查阅相关资料。

第三步:CUB——直接在GPU上表达算法

对于常见算法,手写CUDA核函数不仅容易出错,而且效率往往不尽如人意。无论是设备级模式还是核函数内部的原语,都建议优先使用CUB。

CUB是通过CCCL提供的NVIDIA并行算法库,提供了跨多个粒度的高度优化例程:设备级(`cub::Device*`)、块级(`cub::Block*`)和线程束级(`cub::Warp*`)。

对于RGB转灰度步骤,可以用`cub::DeviceTransform::Transform`替换自定义核函数。该接口接受一组输入迭代器的元组及一个用户自定义函数,将结果写入输出迭代器,整个过程在GPU上执行:

```cpp

cub::DeviceTransform::Transform(

cuda::std::make_tuple(d_image_r, d_image_g, d_image_b),

d_image_gray,

IMAGE_SIZE,

[] __host__ __device__ (pixel_t r, pixel_t g, pixel_t b) {

return static_cast(0.299f * r + 0.587f * g + 0.114f * b);

},

stream);

对于中值计算,手动实现并行块级排序既复杂又低效。可以直接在核函数中使用CUB的块级基数排序:

```cpp

__shared__ typename BlockRadixSort::TempStorage temp_storage;

pixel_t thread_keys[1];

thread_keys[0] = d_image_gray(y, x);

BlockRadixSort(temp_storage).Sort(thread_keys);

if (block_idx.x == TILE_WIDTH / 2 && block_idx.y == TILE_WIDTH / 2)

d_median(grid_block_idx.y, grid_block_idx.x) = thread_keys[0];

再次使用Nsight Systems进行基准测试,结果显示:中值计算时间缩短至773微秒,速度提升了2717倍。三张图像的总计算时间降至635毫秒,整体提速10倍。

重新评估当前瓶颈:内存分配耗时约占图像计算总时长的83%,仍有很大优化空间。

第四步:内存池容器——更便捷、更快速的内存管理

使用`cudaMalloc`分配GPU内存存在两个潜在问题:忘记调用`cudaFree`导致内存泄漏,以及在性能关键路径上内存操作开销过高。

推荐使用CCCL的异步内存容器`cuda::device_buffer`。与C++的`std::vector`类似,容器离开作用域时会自动释放内存,且其底层由内存池支持,反复分配和释放时无需每次都承担`cudaMalloc`/`cudaFree`的完整开销。

代码更新如下:

```cpp

cuda::device_memory_pool_ref device_resource = cuda::device_default_memory_pool(cuda::device_ref{0});

cuda::stream stream{cuda::device_ref{0}};

cuda::device_bufferd_image_r = cuda::make_buffer(stream, device_resource, IMAGE_SIZE, cuda::no_init);

应用此改动后再次分析时间线:内存分配耗时几乎可以忽略不计,单张图像计算速度提升2.6倍。

此时GPU时间已由内存操作主导——三张图像计算的绝大部分时间,都消耗在从CPU向GPU传输红、绿、蓝三个通道的数据上。主机到设备的内存传输还有很大的提速空间。

第五步:固定内存——加速主机到设备的数据传输

CPU的内存分配默认使用可分页内存,GPU无法直接访问。CUDA驱动必须先将数据复制到一块页锁定(固定)的临时缓冲区,再从该缓冲区传输到GPU显存,引入了额外开销。

当CPU内存确定会被传输至GPU时,建议直接使用固定内存分配。CCCL提供了固定内存主机容器`cuda::host_buffer`,可通过`cuda::make_pinned_buffer`工厂函数创建:

```cpp

std::vector> h_images_r(

NB_IMAGES,

cuda::make_pinned_buffer(stream, IMAGE_SIZE, ...));

应用此改动后再次基准测试:主机到设备的内存传输时间大幅缩短,三张图像的总处理时间降至25毫秒,速度提升10倍。

细心的读者可能早已注意到一个现象:尽管使用了多个CPU线程,所有GPU操作(内存操作和核函数)仍然是串行执行的。接下来解决这个问题。

第六步:流——并行化GPU操作

默认情况下,所有操作(核函数、内存分配或传输)都在默认流上启动,可将其视为GPU按顺序执行任务的队列。

本例中,每张图像/线程都需要独立的流。CCCL提供了`cuda::stream`,它是CUDA流的自管理拥有型封装,可在并行for循环内部构造,从而让每个OpenMP线程拥有各自的GPU任务队列。

要充分利用多流的优势,还需使用异步API:CPU提交给GPU的每个操作都不应等待其完成,而应尽可能快地连续提交,以充分饱和GPU。核函数和CUB设备级调用默认已是异步的,会在传入的流上执行。主机与设备之间的异步拷贝,则使用CCCL新提供的`cuda::copy_bytes` API来实现。

在现代CUDA代码中,建议始终使用显式流,避免依赖默认流。

代码更新如下:

```cpp

cuda::stream init_stream{cuda::device_ref{0}};

std::vector> h_images_r(

NB_IMAGES,

cuda::make_pinned_buffer(init_stream, IMAGE_SIZE, ...));

init_stream.sync();

#pragma omp parallel for

cuda::stream stream{cuda::device_ref{0}};

cuda::device_bufferd_image_r = cuda::make_buffer(stream, device_resource, IMAGE_SIZE, cuda::no_init);

cuda::copy_bytes(stream, h_images_r[i], d_image_r);

cub::DeviceTransform::Transform(..., stream.get());

cuda::launch(stream, ...);

cuda::copy_bytes(stream, d_median, h_medians[i]);

stream.sync();

最终时间线显示:核函数与内存拷贝操作现已实现完全重叠。经过全部优化后,三张图像的总处理时间为23毫秒,相比初始的6.8秒,整体提速约300倍。

动手实践

借助CUDA开发者工具箱,我们在不使用任何底层优化手段的前提下,将代码变得更安全、更易维护,同时实现了300倍的性能提升。

欢迎自行尝试这些代码,也可以在Google Colab上直接运行。NVIDIA还提供了完整的工具使用教程课程,已在YouTube上免费发布,并附有Google Colab练习链接。

Q&A

Q1:Compute Sanitizer能检测出哪些类型的CUDA错误?

A:Compute Sanitizer是NVIDIA提供的功能正确性检测工具套件,可以检测多种常见错误,包括共享内存或全局内存的越界读写、非法内存访问、内存竞争条件等。在本文示例中,它直接定位了computeMedian核函数中使用全局索引访问共享内存导致的越界写入错误,而这类错误仅凭肉眼审查代码很难发现。

Q2:CUB的BlockRadixSort相比手写冒泡排序快多少?

A:在本文的测试场景中,将手写单线程冒泡排序替换为CUB的块级基数排序后,中值计算核函数的运行时间从每张图像约2.1秒缩短至773微秒,速度提升了约2717倍。三张图像的总计算时间也从6.8秒降至635毫秒,整体加速约10倍。这一差距主要来自CUB将排序工作并行分配给整个线程块,而非由单一线程串行完成。

Q3:cuda::host_buffer固定内存为什么能加速GPU数据传输?

A:普通CPU内存(可分页内存)在传输到GPU时,CUDA驱动需要先将数据复制到一块临时的页锁定缓冲区,再从该缓冲区传输至GPU显存,相当于多了一次额外拷贝。使用固定内存(即页锁定内存)直接分配,GPU可以通过DMA直接访问该内存,省去了中间环节,从而显著降低传输延迟。在本文示例中,采用cuda::host_buffer后,三张图像的总处理时间从数百毫秒降至约25毫秒,速度提升约10倍。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
匪帅脸色铁青!马竞0-3惨败:1.2亿巨星替补出场遭嘘 赛后主动谢场

匪帅脸色铁青!马竞0-3惨败:1.2亿巨星替补出场遭嘘 赛后主动谢场

风过乡
2026-09-06 09:56:26
印度“要饭哥”又来中国了,只带150元骗吃骗喝。被宁波母女带到家里款待,恶心的一幕发生了...

印度“要饭哥”又来中国了,只带150元骗吃骗喝。被宁波母女带到家里款待,恶心的一幕发生了...

LULU生活家
2026-08-29 09:27:27
苏州小区诅咒式标语风波后续!贴纸连夜清空,张贴者身份依旧成谜

苏州小区诅咒式标语风波后续!贴纸连夜清空,张贴者身份依旧成谜

金哥说新能源车
2026-09-06 01:06:36
随着广州豹1-1,长春亚泰2-0,陕西联合2-1,中甲最新积分榜出炉

随着广州豹1-1,长春亚泰2-0,陕西联合2-1,中甲最新积分榜出炉

俯身冲顶
2026-09-05 21:44:16
头号“国际巨婴”!苏联喂15年,中国养20多年,如今依旧穷得叮当响

头号“国际巨婴”!苏联喂15年,中国养20多年,如今依旧穷得叮当响

历史点行
2026-09-06 03:24:59
人妻街头私服,宽松衫下藏着饱满体态魅力

人妻街头私服,宽松衫下藏着饱满体态魅力

只要高兴就好
2026-09-06 11:34:53
街上越来越多女性不穿胸罩,高跟鞋几乎绝迹:这是身体的解放!

街上越来越多女性不穿胸罩,高跟鞋几乎绝迹:这是身体的解放!

另子维爱读史
2026-08-25 20:02:40
有个瞎子进村讨饭,奶奶给他两个馒头,他临走:大姐,你家要死人了

有个瞎子进村讨饭,奶奶给他两个馒头,他临走:大姐,你家要死人了

古怪奇谈录
2025-08-28 15:43:05
徐海东向毛主席推荐一员猛将,伟人听到名字后却直言:此人不可重用

徐海东向毛主席推荐一员猛将,伟人听到名字后却直言:此人不可重用

海佑讲史
2026-09-06 17:55:05
45岁男子与女友合租一年吃30次他达拉非,一年后房颤,医生:无知

45岁男子与女友合租一年吃30次他达拉非,一年后房颤,医生:无知

垚垚分享健康
2026-09-04 11:20:15
“多带点纸巾!”家长晒一年级男生行李,网友:当你儿子真可怜!

“多带点纸巾!”家长晒一年级男生行李,网友:当你儿子真可怜!

世界圈
2026-09-06 16:20:26
中日若开战,将是地狱模式!全球终于清醒了:中国这次不是在演戏

中日若开战,将是地狱模式!全球终于清醒了:中国这次不是在演戏

叶葉夜
2026-08-31 11:23:21
全红婵近照变化太大了,熬过了最难的发育关,小姑娘彻底长开了,模样跟以前判若两人

全红婵近照变化太大了,熬过了最难的发育关,小姑娘彻底长开了,模样跟以前判若两人

In风尚
2026-08-22 06:04:58
江西24岁女子患病,未婚夫垫付30万治疗费!办完后事准备离开,未来岳父递来一个红袋子:定情物+12万彩礼,一句话让他当场泪崩

江西24岁女子患病,未婚夫垫付30万治疗费!办完后事准备离开,未来岳父递来一个红袋子:定情物+12万彩礼,一句话让他当场泪崩

背包旅行
2026-09-02 18:16:44
落毛凤凰不如鸡!被打倒仅2年,东北雨姐再迎噩耗,彻底不演了

落毛凤凰不如鸡!被打倒仅2年,东北雨姐再迎噩耗,彻底不演了

叹为观止易
2026-08-03 17:07:18
重庆一男子面试被拒,当晚却收到公司主动转账1000元,并被告知这笔钱是作为车马费与面试茶水费,感谢付出。对此,评论区吵翻了...

重庆一男子面试被拒,当晚却收到公司主动转账1000元,并被告知这笔钱是作为车马费与面试茶水费,感谢付出。对此,评论区吵翻了...

励职派
2026-09-04 21:08:21
同积7分却分列第5第6,西甲这场直接对话有点意思

同积7分却分列第5第6,西甲这场直接对话有点意思

坠入温柔晚风
2026-09-06 18:28:32
10万亿转移支付的真相!沿海税源若萎缩,体制内先直面这三件事

10万亿转移支付的真相!沿海税源若萎缩,体制内先直面这三件事

奇思妙想生活家
2026-09-06 04:10:35
郑智暴怒!鼓掌嘲讽主裁判,4分钟两球被吹,戴维森世界波无效

郑智暴怒!鼓掌嘲讽主裁判,4分钟两球被吹,戴维森世界波无效

奥拜尔
2026-09-05 20:43:39
赖岳谦预测,美国最晚2027年、甚至今年内就会对华动手

赖岳谦预测,美国最晚2027年、甚至今年内就会对华动手

人生录
2026-08-13 04:00:04
2026-09-06 19:04:49
至顶科技 incentive-icons
至顶科技
科技产业媒体与 AI 产业服务机构
21579文章数 49729关注度
往期回顾 全部

科技要闻

DeepSeek被曝将采购16万颗华为昇腾950DT

头条要闻

CHINA GT发生重大撞车起火事故 救援人员因为害怕逃离

头条要闻

CHINA GT发生重大撞车起火事故 救援人员因为害怕逃离

体育要闻

本西蒙斯加盟国王,不管怎样,回来就好

娱乐要闻

低调富养!郭富城两女儿入读香港名校

财经要闻

亏损高达200亿,昔日彩电霸主走下巅峰!

汽车要闻

带升降立标MPV 岚图梦想家9预售价42.99万起

态度原创

家居
旅游
健康
亲子
公开课

家居要闻

2026建博会(广州) 公装联探展交流活动

旅游要闻

2026中国旅博会国际化能级持续提升

脑梗取栓成功≠活下来,还有这三关

亲子要闻

幼儿园教育新规落地,找对方向才赢,瞎努力只会拖孩子的后腿

公开课

李玫瑾:为什么性格比能力更重要?

无障碍浏览 进入关怀版