Python 프로그래밍 언어가 시작된 이래로 Python 프로그래밍 언어의 핵심 철학은 코드의 가독성과 단순성을 극대화하는 것이었습니다. Python의 가독성과 단순성을 추구하는 것은 거의 미친 짓입니다. 한 가지 사실은 이를 확인할 수 있습니다. Python 시스템의 루트 디렉토리에 "import this" 명령을 입력하고 Enter 키를 누르면 작은 영어 시가 즉시 인쇄됩니다. 이는 대략 중국어로 번역되면 다음과 같습니다.
"아름다운 것이 추한 것보다 낫고, 명시적인 것이 암시적인 것보다 낫습니다.
복잡한 것보다 단순한 것이 좋고, 복잡한 것보다 복잡한 것이 낫고,
중첩된 것보다 평평한 것이 좋고, 조밀한 것보다 희박한 것이 낫습니다..."
단순한 것보다 낫습니다. 복잡하고 가독성이 중요합니다. Python이 이러한 목표를 달성하는 데 실제로 매우 성공적이라는 것은 의심의 여지가 없습니다. Python은 배우기에 가장 사용자 친화적인 언어이며 일반 Python 프로그램은 일반적으로 동등한 C++ 코드보다 5~10배 더 짧습니다. 불행하게도 문제가 있습니다. Python의 단순성은 성능을 저하시킵니다. 실제로 Python 프로그램은 C++ 프로그램보다 10~100배 느립니다. 따라서 속도와 단순성 사이에는 영구적인 균형이 있는 것으로 보이며 어떤 프로그래밍 언어에서도 두 가지를 모두 갖는 것은 불가능합니다.
하지만 걱정하지 마세요. 모든 희망이 사라지지는 않습니다.
Taichi는 두 세계의 최고를 제공합니다
이 기사에서는 Taichi를 사용하여 작성된 샘플 프로그램을 보여 드리겠습니다. 이 프로그램은 GPU를 사용하여 구에 떨어지는 천 조각의 실시간 물리 시뮬레이션을 수행하는 동시에 결과를 렌더링합니다.
실시간 GPU 물리 시뮬레이터를 작성하는 것은 결코 쉽지 않지만, 이 루틴을 구현하는 Taichi 소스 코드는 매우 간단합니다. 이 기사의 나머지 부분에서는 Taichi가 제공하는 기능과 이것이 얼마나 강력하고 사용자 친화적인지 느낄 수 있도록 전체 구현 과정을 안내합니다.
시작하기 전에 이 프로그램이 몇 줄의 코드로 구성되어 있는지 추측해 보세요. 물론 기사 끝부분에서 답을 찾을 수 있습니다.
알고리즘 개요
이 대량 스프링 시스템의 움직임은 4가지 요소의 영향을 받습니다.
중력프로그램이 시작됩니다
Python 프로그램에서 Taichi를 사용하려면 먼저 별칭 ti를 사용하여 Taichi를 가져와야 합니다.
import taichi as ti
컴퓨터에 CUDA를 지원하는 Nvidia GPU가 있는 경우 Taichi 프로그램의 성능이 최대화됩니다. 이 경우 위 import 문 뒤에 다음 코드 줄을 추가하세요.
ti.init(arch=ti.cuda)
CUDA GPU가 없어도 ti.metal, ti와 같은 다른 그래픽 API를 통해 Taichi를 계속 사용할 수 있습니다. vulkan 및 ti.opengl GPU 상호 작용. 그러나 이러한 API에 대한 Taichi의 지원은 CUDA에 대한 지원만큼 포괄적이지 않습니다. 따라서 지금은 컴퓨팅 백엔드로 CPU를 사용하고 있습니다.
ti.init(arch=ti.cpu)
걱정하지 마세요. Taichi는 CPU에서만 실행되더라도 빠르게 실행됩니다. Taichi를 초기화한 후 대량 스프링 천을 설명하는 데 사용되는 데이터 구조 선언을 시작할 수 있습니다. 이를 위해 다음 코드 줄을 추가합니다.
N = 128 x = ti.Vector.field(3, float, (N, N)) v = ti.Vector.field(3, float, (N, N))
이 세 줄은 x와 v를 N×N 크기의 2차원 배열로 선언합니다. 여기서 배열의 각 요소는 부동 소수점 숫자의 3차원 벡터입니다. Taichi에서는 배열을 "필드"라고 부르며 두 필드는 각각 점 질량의 위치와 속도를 기록합니다. CUDA GPU에서 실행되도록 Taichi를 초기화하는 경우 이러한 필드/배열은 자동으로 GPU 메모리에 저장됩니다. 천 외에도 중앙에 공을 정의해야 합니다.
ball_radius = 0.2 ball_center = ti.Vector.field(3, float, (1,))
여기서 공의 중심은 크기 1의 1D 필드이고 단일 구성 요소는 3D 부동 벡터입니다. 필수 필드를 선언한 후 t=0의 해당 데이터로 이러한 필드를 초기화하겠습니다. 동일한 행이나 열에 있는 인접한 점 쌍에 대해 그 사이의 거리가 cell_size=1.0/N과 동일한지 확인하려고 합니다. 이는 다음 초기화 루틴을 통해 달성됩니다.
def init_scene(): for i, j in ti.ndrange(N, N): x[i, j] = ti.Vector([i * cell_size, j * cell_size / ti.sqrt(2), (N - j) * cell_size / ti.sqrt(2)]) ball_center[0] = ti.Vector([0.5, -0.5, 0.0])
여기서 각 x[i,j] 값의 의미에 대해 걱정할 필요가 없습니다. 천이 45도 모서리에 떨어지도록 선택되었을 뿐입니다. 아래 이미지로 .
在每个时间步中,我们的程序都会模拟影响布料运动的4个因素:重力、弹簧内力、阻尼和与红球的碰撞。其中,重力是最容易处理的。
下面是实现这一点的代码:
@ti.kernel def step(): for i in ti.grouped(v): v[i].y -= gravity * dt
这里有两点需要注意。首先,语句for i in ti.grouped(x)意味着将循环迭代x的所有元素,而不管x中有多少维度。其次,也是最重要的是:注解@ti.kernel意味着Taichi将自动并行运行函数中的任何顶级for循环。在本例中,Taichi将并行更新v中每个N*N向量的y分量。
接下来,我们来处理弦线的内力计算问题。首先,请注意前面图形中的每个质点最多连接到八个邻接质点。这些连接在我们的程序中表示如下:
links = [[-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [1, -1], [-1, 1], [1, 1] links = [ti.Vector(v) for v in links]
从物理角度来看,系统中的每个弹簧s都用固定长度l(s,0)初始化。在任何时间t,如果s的当前长度l(s,t)超过l(s,0),则弹簧将在其端点上施加力,将它们拉在一起。相反,如果l(s,t)小于l(s,0),则弹簧会将端点彼此推开。这些力的大小始终与l(s,0)-l(s,0)的绝对值成正比。此交互由以下代码段捕获:
for i in ti.grouped(x): force = ti.Vector([0.0,0.0,0.0]) for d in ti.static(links): j = min(max(i + d, 0), [N-1,N-1]) relative_pos = x[j] - x[i] current_length = relative_pos.norm() original_length = cell_size * float(i-j).norm() if original_length != 0: force +=stiffness * relative_pos.normalized() * (current_length - original_length) / original_length v[i] +=force * dt
请注意,这个for循环仍应作为substep函数中的顶级for循环,该函数用@ti.kernel注解。这样可以确保并行计算施加到每个质点的弹簧力。stiffness在此是一个常数,用于控制弹簧长度变化的程度。在上述程序中,我们使用stiffness =1600指定它的值。在现实世界中,当弹簧振动时,弹簧中储存的能量会消散到周围环境中,其振动最终停止。为了捕捉这种效应,在每个时间步,我们稍微降低每个点的速度大小:
for i in ti.grouped(x): v[i] *= ti.exp(-damping * dt)
在此,damping取固定值2。
我们还需要处理布料和红球之间的碰撞。要做到这一点,我们只需将质点与球接触时的速度降低到0。这样可以确保布料“挂”在球上,而不是穿透球或向下滑动:
if (x[i]-ball_center[0]).norm() <= ball_radius: v[i] = ti.Vector([0.0, 0.0, 0.0])
最后,我们用每个质点的速度更新其自身的位置:
x[i] += dt * v[i]
这就是我们对一块质量弹簧布料进行并行模拟所需的全部代码。
我们将使用Taichi内置的基于GPU的GUI系统(昵称是“GGUI”)渲染布料。GGUI使用Vulkan图形API进行渲染,因此请确保您的计算机上安装了Vulkan(https://docs.taichi.graphics/lang/articles/misc/ggui)。GGUI支持渲染两种类型的3D对象:三角形网格和粒子。在我们的示例中,将把布料渲染为三角形网格,把红色球渲染为单个粒子。
GGUI表示一个三角形网格,包含两个Taichi场:一个顶点(vertices)场和一个索引(indices)场。顶点场是一个一维场,其中每个元素提取是一个表示顶点位置的三维向量,可能由多个三角形共享。在我们的应用程序中,每个点质量都是一个三角形顶点,因此我们可以简单地将数据从x复制到vertices:
vertices = ti.Vector.field(3, float, N * N) @ti.kernel def set_vertices(): for i, j in ti.ndrange(N, N): vertices[i * N + j] = x[i, j]
请注意,每一帧都需要调用set_vertices,因为顶点位置不断被模拟更新。
我们的布料是用一个质点的N×N网格表示,也可以被看作一个由(N-1)×(N-1)小正方形组成的网格。每个正方形都将渲染为两个三角形。因此,总共有(N-1)×(N-1)×2个三角形。每个三角形将在顶点场中表示为3个整数,该场记录顶点场中三角形顶点的索引。以下代码片段捕获了这一结构:
num_triangles = (N - 1) * (N - 1) * 2 indices = ti.field(int, num_triangles * 3) @ti.kernel def set_indices(): for i, j in ti.ndrange(N, N): if i < N - 1 and j < N - 1: square_id = (i * (N - 1)) + j #正方形的第一个小三角形 indices[square_id * 6 + 0] = i * N + j indices[square_id * 6 + 1] = (i + 1) * N + j indices[square_id * 6 + 2] = i * N + (j + 1) #正方形的第二个小三角形 indices[square_id * 6 + 3] = (i + 1) * N + j + 1 indices[square_id * 6 + 4] = i * N + (j + 1) indices[square_id * 6 + 5] = (i + 1) * N + j
请注意,与函数set_vertices不同,函数set_indices只需要调用一次。这是因为三角形顶点的索引实际上并没有改变——只是位置在改变。
为了将红球渲染为粒子,我们实际上不需要准备任何数据,我们之前定义的ball_center和ball_radius变量就是GGUI所需要的全部内容。
至此,我们已经介绍完本文示例程序的所有核心函数!下面代码展示了我们如何调用这些函数:
init() set_indices() window = ti.ui.Window("Cloth", (800, 800), vsync=True) canvas = window.get_canvas() scene = ti.ui.Scene() camera = ti.ui.make_camera() while window.running: for i in range(30): step() set_vertices() camera.position(0.5, -0.5, 2) camera.lookat(0.5, -0.5, 0) scene.set_camera(camera) scene.point_light(pos=(0.5, 1, 2), color=(1, 1, 1)) scene.mesh(vertices, indices=indices, color=(0.5, 0.5, 0.5), two_sided = True) scene.particles(ball_center, radius=ball_radius, color=(0.5, 0, 0)) canvas.scene(scene) window.show()
需要注意的一个小细节是,我们将在主程序循环中的每一帧调用函数step()30次,而不是调用一次。这样做的目的就是让动画不会运行得太慢。把上述所有代码放在一起,整个程序应该是这样的:
import taichi as ti ti.init(arch=ti.cuda) # 另一种可选择方案: ti.init(arch=ti.cpu) N = 128 cell_size = 1.0 / N gravity = 0.5 stiffness = 1600 damping = 2 dt = 5e-4 ball_radius = 0.2 ball_center = ti.Vector.field(3, float, (1,)) x = ti.Vector.field(3, float, (N, N)) v = ti.Vector.field(3, float, (N, N)) num_triangles = (N - 1) * (N - 1) * 2 indices = ti.field(int, num_triangles * 3) vertices = ti.Vector.field(3, float, N * N) def init_scene(): for i, j in ti.ndrange(N, N): x[i, j] = ti.Vector([i * cell_size , j * cell_size / ti.sqrt(2), (N - j) * cell_size / ti.sqrt(2)]) ball_center[0] = ti.Vector([0.5, -0.5, -0.0]) @ti.kernel def set_indices(): for i, j in ti.ndrange(N, N): if i < N - 1 and j < N - 1: square_id = (i * (N - 1)) + j # 1st triangle of the square indices[square_id * 6 + 0] = i * N + j indices[square_id * 6 + 1] = (i + 1) * N + j indices[square_id * 6 + 2] = i * N + (j + 1) # 2nd triangle of the square indices[square_id * 6 + 3] = (i + 1) * N + j + 1 indices[square_id * 6 + 4] = i * N + (j + 1) indices[square_id * 6 + 5] = (i + 1) * N + j links = [[-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [1, -1], [-1, 1], [1, 1]] links = [ti.Vector(v) for v in links] @ti.kernel def step(): for i in ti.grouped(x): v[i].y -= gravity * dt for i in ti.grouped(x): force = ti.Vector([0.0,0.0,0.0]) for d in ti.static(links): j = min(max(i + d, 0), [N-1,N-1]) relative_pos = x[j] - x[i] current_length = relative_pos.norm() original_length = cell_size * float(i-j).norm() if original_length != 0: force +=stiffness * relative_pos.normalized() * (current_length - original_length) / original_length v[i] +=force * dt for i in ti.grouped(x): v[i] *= ti.exp(-damping * dt) if (x[i]-ball_center[0]).norm() <= ball_radius: v[i] = ti.Vector([0.0, 0.0, 0.0]) x[i] += dt * v[i] @ti.kernel def set_vertices(): for i, j in ti.ndrange(N, N): vertices[i * N + j] = x[i, j] init_scene() set_indices() window = ti.ui.Window("Cloth", (800, 800), vsync=True) canvas = window.get_canvas() scene = ti.ui.Scene() camera = ti.ui.make_camera() while window.running: for i in range(30): step() set_vertices() camera.position(0.5, -0.5, 2) camera.lookat(0.5, -0.5, 0) scene.set_camera(camera) scene.point_light(pos=(0.5, 1, 2), color=(1, 1, 1)) scene.mesh(vertices, indices=indices, color=(0.5, 0.5, 0.5), two_sided = True) scene.particles(ball_center, radius=ball_radius, color=(0.5, 0, 0)) canvas.scene(scene) window.show()
注意到,上述代码总行数仅有91行!
我希望你喜欢本文中提供的上述示例程序!如果的确如此,下面几个不同挑战等级的任务留给你:
마지막으로 Taichi가 위의 91줄 Python 코드에서 구현할 수 있도록 허용한 내용을 검토해 보겠습니다.
Taichi를 사용하면 소량의 작업으로 이러한 모든 복잡한 기능을 구현할 수 있을 뿐만 아니라 하지만 CUDA, 멀티스레드 프로그래밍 또는 GPU 렌더링을 배우는 수고를 덜어줍니다. Taichi를 사용하면 누구나 고성능 프로그램을 작성할 수 있습니다. 코드의 알고리즘 측면에 집중하고 성능 측면은 프로그래밍 언어 자체에 맡길 수 있습니다. Taichi의 모토는 다음과 같습니다: 모두를 위한 병렬 프로그래밍
Taichi에 대해 자세히 알아보려면 Taichi에 대해 자세히 알아보려면 Github 페이지를 방문하세요. Taichi 프로젝트의 많은 예가 있는데 모두 매우 흥미롭습니다. 마지막으로, 병렬 컴퓨팅을 위한 친숙하고 강력한 언어를 개발한다는 사명을 믿는다면 Taichi에 오픈 소스 기여자로 합류하는 것을 환영합니다.
다음 기사에서는 Taichi의 내부 작동 방식과 Taichi가 계산 및 렌더링을 위해 다양한 플랫폼에서 GPU와 상호 작용하는 방법에 대해 논의하겠습니다. 그때쯤이면 행복한 타이치 프로그래밍이 시작될 것입니다!
Zhu Xianzhong, 51CTO 커뮤니티 편집자, 51CTO 전문 블로거, 강사, 웨이팡 대학의 컴퓨터 교사이자 프리랜서 프로그래밍 업계의 베테랑입니다. 초창기에는 다양한 Microsoft 기술에 집중했습니다(ASP.NET AJX 및 Cocos 2d-X 관련 기술 서적 3권 집필). 지난 10년 동안 그는 오픈 소스 세계에 전념했습니다(인기 있는 풀 서비스 기술에 익숙함). 스택 웹 개발 기술)을 배우고 OneNet/AliOS+Arduino. /ESP32/Raspberry Pi 등 IoT 개발 기술과 Scala+Hadoop+Spark+Flink 등 빅데이터 개발 기술에 대해 배웠습니다.
원제: Python의 고성능 컴퓨팅을 위한 초보자 가이드, 저자: Dunfan Lu
위 내용은 Taichi 기반 Python을 사용한 고성능 컴퓨팅 입문 가이드의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!