LeetGPU习题03:Color inversion

2578 字
13 分钟
LeetGPU习题03:Color inversion
2026-04-27
已完成

算子说明#

名称说明
Vector Addition两个向量逐元素相加
Matrix Addition两个矩阵逐元素相加
Matrix Copy逐元素复制矩阵
Color Inversion对每个像素独立取反
Reverse Array反转数组,每个元素独立移动位置
ReLU逐元素应用 ReLU 函数
Leaky ReLU逐元素应用 Leaky ReLU
Sigmoid Activation逐元素应用 Sigmoid 函数
Value Clipping逐元素裁剪到指定范围
Sigmoid Linear Unit (SiLU)逐元素 SiLU 激活
Swish-Gated Linear Unit (SWiGLU)逐元素 SWiGLU(门控部分也为逐元素)
Gaussian Error Gated Linear Unit (GEGLU)逐元素 GEGLU 激活
RGB to Grayscale每个像素独立转换,不依赖邻域
Interleave Arrays交替合并两数组,每个输出元素仅依赖对应位置输入
Rotary Positional Embedding对每个位置独立应用旋转矩阵
Weight Dequantization每个权重独立反量化
INT8 Quantized MatMul(仅反量化部分)反量化部分为逐元素,整体不是
Simple Inference线性层前向包含矩阵乘,非 element-wise,但其中的激活部分可能是逐元素

1. 颜色反转#

题目描述
给定一张图像的 RGBA 数据,以一维数组形式存储。每个像素由四个 8 位无符号整数(unsigned char)组成,依次为 R(红)、G(绿)、B(蓝)、A(透明度)。
颜色反转的规则:将每个像素的 R、G、B 分量用 255 减去原值,A 分量保持不变。
数组 image 的长度为 width * height * 4,依次存放所有像素的 RGBA 值(左上角像素为前 4 个元素)。

请实现函数 solve,直接在输入的 image 张量上进行修改,完成颜色反转。

注意

  • 输入张量 image 位于 GPU 上,数据类型为 torch.uint8(即无符号 8 位整数)。
  • 你只能使用 PyTorch 原生功能,不允许引入外部库。
  • 函数签名不可修改。
  • 最终结果必须写回 image 数组本身。

示例 1

输入: image = [255, 0, 128, 255, 0, 255, 0, 255], width = 1, height = 2
输出: [0, 255, 127, 255, 255, 0, 255, 255]

示例 2

输入: image = [10, 20, 30, 255, 100, 150, 200, 255], width = 2, height = 1
输出: [245, 235, 225, 255, 155, 105, 55, 255]

约束条件

  • 1 ≤ width ≤ 4096
  • 1 ≤ height ≤ 4096
  • width * height ≤ 8,388,608 (即最多约 8M 像素)
  • 性能测试场景:height = 5120, width = 4096(约 2 千万像素)

提示
你可以利用 PyTorch 的向量化操作(如切片、算术运算)高效实现,无需逐像素循环。注意 torch.uint8 的减法可能会溢出,应使用适当的方式(如 255 - imageimage ^= 0xFF 等)。

2. Pytorch题解#

def solve(image: torch.Tensor, width: int, height: int):
pixels = image.view(-1, 4)
pixels[:, :3] = ~pixels[:, :3]

只有两行代码,看起来非常简单,但是这些究竟是如何高效实现的呢?

其实在深度学习中,viewreshape,包括对于张量的切片操作是经常要用的,我们来仔细学习一下如何使用。

以下是我要问的几个问题:

  1. 为什么要用view而不是reshape?
  2. 如何高效使用切片?

2.1. 零拷贝view#

view是Pytorch中的改变张量形状的方法之一。 它并不会复制数据,只是改变我们看待底层内存的数据方式。

举个栗子:

import torch
a = torch.arange(12)
print(a)

此时你有一块连续的12个int的内存,它是一维的,但是此刻我们想把它看作一个三行四列的行列式(矩阵)。

b = a.view(3, 4)
print(b)

示例
示例

此时a和b共享同一块存储,修改b中的元素,a也会跟着变。

修改第一个元素
修改第一个元素

这就是view的功效。

它有一个严格的前提条件:张量的内存必须是连续的。 也就是说,数据在底层是按照顺序排列的,没有间隙。

什么叫没有间隙?我们来看一个例子:

# a = [0,1,2,3,4,5....] 假设a是这样子的
a = torch.arange(12).reshape(3, 4)
b = a.t()
print(a.is_contiguous())
print(b.is_contiguous())
b.view(-1)

为什么转置后就不连续了呢?因为a的数据实际上是按照行优先存储的。 转置成b后,如果你想按照行取出b[0],实际上就变成了[0, 4, 8]

这三个数字在内存中并不相邻,中间隔着其他元素。这种跳跃式访问意味着它并不是一块连续的内存块。

此时view就失效了。

把一个矩阵进行转置
把一个矩阵进行转置

所以我们需要把它先调用.contiguous()将其复制成为一块连续的内存,再view

b.contiguous().view(-1)
一句话

view只要数据在内存中不连续,例如经过转置、切片等操作,就会失败。需要使用contiguous来让数据重新连续化。

view和reshape的区别是什么?

如果代码是你自己写的,知道内存是否连续,在乎性能,就用view;否则就用reshape。

注意

reshape很有可能让梯度消失。

2.2. 切片,真的有这么丝滑吗?#

学习了view之后,那么如何随心所欲的切片呢? 这是Python最为强大的特性。

我们来一口气学会所有的切片方案。

# 区间是[)左闭右开。(高中的知识)
tensor[start:stop:step] # 起始、终点、间隔

这是切片的基本公式。

:表示到,::表示隔几个

一维切片#

import torch
# 创建 0 到 9 的向量
a = torch.arange(10) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 常见操作
print(a[2:5]) # [2, 3, 4] 从2到5
print(a[:5]) # [0, 1, 2, 3, 4] 从0到5
print(a[5:]) # [5, 6, 7, 8, 9] 5到最后
print(a[::2]) # [0, 2, 4, 6, 8] 每隔两个取一个
print(a[::-1]) # [9, 8, 7... 0] 倒着每隔一个取一个(倒序)
print(a[None, :])# shape(1, 10) 躺平变“行向量” (增加一个虚维度)

多维切片#

这才是让新手最为头疼的部分,既不知道怎么切,又不知道为什么这么写。

核心规则:
  1. 如果你用数字切片,发生降维
  2. 如果你用冒号切片,则维度保留
# 创建 (3, 4, 5) 张量
x = torch.arange(60).reshape(3, 4, 5)
# 1. 降维取法
# 第一维消失,结果是 (4, 5) 的矩阵
result1 = x[0]
# 2. 跨层取法
# 第二个维度都取第一个元素,结果是 (3, 5)
result2 = x[:, 0]
# 3. 区域取法
# 第一维不变,第二维取1:3, 第三维取1:4。结果是 (3, 2, 3)
result3 = x[:, 1:3, 1:4]
# 4. 终极武器:省略号 ...
# “不管前面有多少维,我只要最后一维的第 2 列”。
# 等价于 x[:, :, 2],结果形状 (3, 4)
result4 = x[..., 2]

切片示意图
切片示意图

3. Triton题解#

在实现图像反色时,我最初尝试引入 @triton.autotune 来自动寻优。但这里隐藏着一个坑:原地修改与 Autotune 的冲突。

autotune 的机制是通过在后台多次运行核函数来分析不同配置的性能。由于我们的反色操作是原地计算(255 - val),如果 kernel 被执行了偶数次,图像就会被反色两次,从而变回原始图像,导致最终输出错误的结果。因此,针对这种 In-place 操作,我选择手动指定块大小,没有使用 autotune。

完整的 Kernel 代码如下:

import torch
import triton
import triton.language as tl
@triton.jit
def invert_kernel(
image_ptr,
width,
height,
BLOCK_SIZE_X: tl.constexpr,
BLOCK_SIZE_Y: tl.constexpr
):
pid_x = tl.program_id(0)
pid_y = tl.program_id(1)
rx = pid_x * BLOCK_SIZE_X + tl.arange(0, BLOCK_SIZE_X)
ry = pid_y * BLOCK_SIZE_Y + tl.arange(0, BLOCK_SIZE_Y)
# rx[:, None] 是 (BLOCK_SIZE_X, 1),ry[None, :] 是 (1, BLOCK_SIZE_Y)
mask = (rx[:, None] < height) & (ry[None, :] < width)
# 计算 1D 数组中的基础偏移量:(row * width + col) * 4(因为是RGBA)
# 这里利用了广播机制
base_offsets = (rx[:, None] * width + ry[None, :]) * 4
for i in range(3):
ptr = image_ptr + base_offsets + i
val = tl.load(ptr, mask=mask)
tl.store(ptr, 255 - val, mask=mask)
def solve(image: torch.Tensor, width: int, height: int):
BLOCK_SIZE_X = 32
BLOCK_SIZE_Y = 32
# 计算网格大小
grid = (
triton.cdiv(height, BLOCK_SIZE_X),
triton.cdiv(width, BLOCK_SIZE_Y)
)
# 启动核函数
invert_kernel[grid](
image,
width,
height,
BLOCK_SIZE_X=BLOCK_SIZE_X,
BLOCK_SIZE_Y=BLOCK_SIZE_Y,
num_warps=8
)
return image

对于二维矩阵的代码,我们发现对于rx和ry来说,他们的计算方式一模一样,我们先生成对应的线程块。

rx表示所有的行号,ry表示所有的列号。

之后就需要计算mask与offset。

mask = (rx[:, None] < height) & (ry[None, :] < width) 这里就用到了升维和广播机制。

  • rx[:, None]变成了(32, 1)
  • ry[None,:]变成了(1, 32)
  • rx[:, None] < height: 每一行都去比对是否越界。
  • 逻辑交集 (&):当列向量和行向量做运算时,Triton 会自动将其广播成一个 (32, 32) 的布尔矩阵。

图像在 GPU 显存中是一段连续的一维数组,而不是二维矩阵。因此我们需要把 (rx, ry) 这个二维逻辑坐标展平为一维偏移量: base_offsets = (rx[:, None] * width + ry[None, :]) * 4

这段代码的背后包含了三个步骤:

  1. 行定位:rx[:, None] * width 拿到了当前行的开头元素在整张图像中的绝对索引。

  2. 列偏移:加上 ry[None, :] 后,我们就得到了该像素在整体一维连续空间中的逻辑索引 (row * width + col)。注意,这里同样利用了广播机制,生成了一个 (32, 32) 的索引矩阵。

  3. 通道步长:由于我们处理的是 RGBA 图像,每个像素包含 R、G、B、A 四个值,在显存中占据 4 个连续的元素位置。因此,我们将上述像素索引整体乘以 4,得到了每个像素首通道的准确内存地址 base_offsets。

4. CUDA题解#

__global__ void invert_naive(unsigned char* image, int N) {
int tid = blockIdx.x * blockDim.x + threadIdx.x;
if (tid < N) {
image[tid] = 255 - image[tid];
}
}
__global__ void invert_stride(unsigned char* image, int N) {
int tid = blockIdx.x * blockDim.x + threadIdx.x;
int stride = gridDim.x * blockDim.x;
for (int i = tid; i < N; i += stride) {
image[i] = ~image[i]; // ~x == 255 - x for unsigned char
}
}
__global__ void invert_rgba_uchar4(unsigned char* image, int num_pixels) {
int tid = blockIdx.x * blockDim.x + threadIdx.x;
int stride = gridDim.x * blockDim.x;
uchar4* img4 = (uchar4*)image;
for (int i = tid; i < num_pixels; i += stride) {
uchar4 p = img4[i];
p.x = 255 - p.x;
p.y = 255 - p.y;
p.z = 255 - p.z;
// p.w (alpha) 保持不变
img4[i] = p;
}
}
__global__ void invert_2D(unsigned char* image, int width, int height) {
int row = blockIdx.y * blockDim.y + threadIdx.y;
int col = blockIdx.x * blockDim.x + threadIdx.x;
if (col < width && row < height) {
int idx = (row * width + col) * 4;
image[idx + 0] = 255 - image[idx + 0];
image[idx + 1] = 255 - image[idx + 1];
image[idx + 2] = 255 - image[idx + 2];
}
}

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
LeetGPU习题03:Color inversion
https://dlog.com.cn/posts/leetgpu03/matrix_copy/
作者
杜子源
发布于
2026-04-27
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
杜子源
都是风景,幸会
公告
请狠狠地打赏我,打赏一次,爆更一篇!!
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
17
分类
6
标签
8
总字数
27,300
运行时长
0
最后活动
0 天前

目录