diff --git a/_posts/2023-09-25-mojo.md b/_posts/2023-09-25-mojo.md new file mode 100644 index 00000000..d2fce751 --- /dev/null +++ b/_posts/2023-09-25-mojo.md @@ -0,0 +1,355 @@ +--- +layout: post +title: "Mojo Overview" +date: 2023-09-25 +author: yhunroh +tags: [Python, software-design, introduction] +--- + +## 소개 + +https://www.modular.com/mojo + +Mojo는 파이썬의 생태계를 그대로 흡수하면서 C와 비견할 만한 성능과 low-level 기능들까지 갖추는 것을 지향하는 언어입니다. 주로 AI 연구 및 서비스, 데이터 분석 및 처리를 타겟층으로 하여 개발되고 있습니다. + +Mojo는 Modular라는 기업에서 개발하고 있으며, Co-Founder인 Chris Lattner는 Swift, LLVM, Clang, MLIR를, 또 다른 Co-founder인 Tim Davis는 Tensorflow, Android ML에서 각각 주도적인 역할을 한 것으로 알려져 있습니다. 특히 수십명에 달하는 Modular 팀에 AI Infra, Dev 직군이 무척 많다는 것으로부터 현재 팀의 방향성은 AI 생태계를 개선하는 데에 집중하고 있다는 점을 확인할 수 있습니다. + +Mojo는 2022년 상반기부터 본격적으로 개발되기 시작한 것으로 보이고, 2023년 5월에 웹 데모 playground가 공개되고, 2023년 9월에 리눅스에서 다운로드가 가능한 0.3.0 버전의 SDK가 공개되었습니다. SDK 공개가 대중에게 첫 런칭한 것이니만큼, 다양한 행사와 PR을 준비한 것을 볼 수 있습니다. +[9월 런칭 키노트 영상](https://youtu.be/-3Kf2ZZU-dg?si=m4KUv5ocvI2RZOlE) + +## 기능 및 로드맵 + +현재 Mojo가 내걸고 있는 주요 기능들을 요약하면 다음과 같습니다. + +- Progressive Types, Zero Cost Abstractions + - 별도 설정 없이도 정적 타이핑을 지원하며, generic type 또한 지원합니다. + - 이러한 generic type를 실제 타입으로 해석하는 것이 컴파일 시간에 이루어져, 런타임에서의 타협이 없습니다. +- Ownership & Borrow Checker + - 변수를 함수의 인자로 넘겨줄 때 ownership을 지정할 수 있고, 컴파일러 레벨에서 관리합니다. + - 러스트와 비슷하게 Ownership 여부에 따라 변수를 복사해서 넘길지, reference를 넘길지가 결정되고, 이에 따라 concurrency 등의 기능을 더 편리하게 사용할 수 있습니다. +- Portable Parametric Algorithms, Language Integrated Auto-Tuning + - 함수를 정의할 때 하드웨어에 의존하는 parameter, 값을 변경하면서 실험하고자 하는 hyperparamter 등을 런타임 시간 타협 없이 사용할 수 있습니다. + - 필요한 경우, 이러한 parameter를 바꿔가면서 최적화하는 기능이 내장되어 있습니다. +- MLIR, Parallel Heterogeneous Runtime + - JVM 혹은 LLVM-IR처럼, 구동 하드웨어 및 플랫폼에 무관하게 동일한 수준의 최적화를 보장합니다. + - 또한 Mojo 코드 레벨에서 MLIR 기능을 활용할 수 있어, 더 높은 수준의 최적화가 필요할 경우 직접 구현할 수 있습니다. + - CPU의 아키텍쳐와 무관하게 MLIR 레벨까지 코드를 공유하기 때문에, 서로 다른 아키텍쳐를 가진 코어 사이에서도 병렬처리가 가능합니다. + +이외에도 주목할만한 차별점들은 강조하면 다음과 같습니다. + +- 메모리를 직접 관리할 수 있음 + - 포인터, alloc, memset 등 저수준 메모리 기능들이 있어 C에서 가능한 만큼의 안정성과 효율성을 제공합니다. +- 파이썬을 직접 구동할 수 있음 & 파이썬의 superset + - Python.import_module로 임의의 파이썬 모듈을 불러와서 사용할 수 있으며, Jupyter Kernel에서는 아예 파이썬으로 셀을 실행하는 기능도 있습니다. +- simd, tensor 등 타입을 내장으로 지원함 + - 당연히 fma, reduce 등의 주요 연산들의 저수준까지의 최적화가 잘 되어 있으며, simd와 tensor 사이의 전환도 기본적으로 지원합니다. +- 컴파일 유연성 + - JIT과 AOT 컴파일 모두 지원하며, 파이썬처럼 REPL 또한 있습니다. + - MLIR의 특성을 통해 gcc 등의 기존 컴파일러와는 다르게 cpu, gpu 뿐만 아니라 주문 제작 칩, 양자 컴퓨팅 칩 등을 대상으로 컴파일할 수 있도록 세팅 가능합니다. + +추가적으로, Modular는 Mojo 이외에도 파이썬과 C에서 사용할 수 있는 Modular AI engine을 제공합니다. Modular AI Engine이 내걸고 있는 특장점은 다음과 같습니다. + +- Unification, Compatibility, Integration + - 기존에 사용되고 있는 인프라 (AWS, GCP / ARM, x86, Nvidia 등), AI 프레임워크 (TensorFlow, Pytorch, XGBoost 등) 와 함께 바로 활용 가능합니다. + - 이에 따라 파이프라인 통합 및 개발 스택 일원화가 가능합니다. +- Performance + - 파이썬에서 기존 AI 프레임워크들보다 더 빠른 Modular AI 엔진을 제공합니다. + +위의 특장점이 있지만, Mojo는 아직 신생 언어이기 때문에 아직 부족한 점들 또한 존재합니다. Modular는 이를 잘 인지하고 있으며, 이를 갈무리하여 로드맵과 현재 Mojo를 사용할 때의 주의사항 또한 공유하고 있습니다. + +- 아쉬운 SDK + - 리눅스만 지원하며, 설치 과정이 안정적이지 않음 +- 부족한 Syntatic sugar +- Ownership & Lifetime + - Closure 내부에서 정의된 값들에 대한 ownership 처리 + - 제대로 된 Lifetime 기능 지원 +- Protocol / Traits 지원 + - Swift의 Protocol 혹은 Rust의 Traits에 대응되는 기능 지원 +- Class 지원 + - 현재는 컴파일 타임에 모두 정의되야만 하는 struct만 지원 + - 파이썬에서의 동작과 동일하게 완전 dynamic한 class를 디폴트로 지원할 것인지, 아니면 Swift와 비슷하게 선택적으로 dynamic class를 사용할 수 있도록 할 것인지 논의중 +- C, C++ interop + - Clang은 LLVM을 통해 MLIR로 컴파일할 수 있기 때문에, 높은 수준의 interoperability를 제공할 수 있음 + +구체적인 예시들은 [Sharp Edges](Capture declarations in closures) 로 열거해두고 있습니다. + +--- + +## 사용 예시 + +Mojo의 리눅스 SDK를 공개하면서, Mojo의 특장점을 구체적으로 알리기 위해 다양한 예시 코드들 또한 공개되었습니다. [Mojo Github](https://github.com/modularml/mojo/tree/main/examples/notebooks) 에서 예시들을 확인할 수 있는데, 현재 제공된 예시들은 다음과 같습니다. + +- Mandelbrot set 계산하고 그리기 +- 행렬곱 계산하기 +- memset 구현하기 +- Ray tracing 구현하기 +- Low-level IR로 새로운 자료형 구현하기 + +이 글에서는 Mandelbrot set을 계산하는 예시 코드와, mojo로 구현한 세그트리 코드를 사용해 mojo를 이해해 보겠습니다. + +### Mandelbrot + +이 코드에서 구하고자 하는 것은, 2차원 복소평면 상에서 각 점 z0가 다음의 과정을 거쳤을 때, 원점에서 거리 2 초과로 멀어지는지, 멀어진다면 그때까지 필요한 시행의 횟수입니다. $$z_{i+1} = z_i ^2 + z_0 $$ +어렵지 않게 파이썬으로 이 알고리즘을 구현할 수 있지만, 사용할 수 있는 mojo의 기능들을 통해 가능한 한 빠르게 구현된 코드를 분석해보도록 합시다. + +```python +from benchmark import Benchmark +from complex import ComplexSIMD, ComplexFloat64 +from math import iota +from python import Python +from runtime.llcl import num_cores, Runtime +from algorithm import parallelize, vectorize +from tensor import Tensor +from utils.index import Index + +alias float_type = DType.float64 +alias simd_width = 2 * simdwidthof[float_type]() + +alias width = 960 +alias height = 960 +alias MAX_ITERS = 200 + +alias min_x = -2.0 +alias max_x = 0.6 +alias min_y = -1.5 +alias max_y = 1.5 +``` + +기본적인 import 문법은 파이썬과 비슷합니다. 다만, 현재 mojo가 지원하는 라이브러리들의 리스트와 명세는 [docs.modular.com](docs.modular.com) 에서 확인할 수 있습니다. + +`alias` 키워드는 컴파일 시간에 정의되는 값들을 나타냅니다. C에서의 `#define` 과 비슷하게 이해할 수 있는 듯 합니다. +min_x 등의 값들은 구하고자 하는 복소평면상의 범위, width와 height는 해당 범위에서 계산할 점의 개수를 의미합니다. + +```python +fn mandelbrot_kernel_SIMD[ + simd_width: Int +](c: ComplexSIMD[float_type, simd_width]) -> SIMD[float_type, simd_width]: + + let cx = c.re + let cy = c.im + var x = SIMD[float_type, simd_width](0) + var y = SIMD[float_type, simd_width](0) + var y2 = SIMD[float_type, simd_width](0) + var iters = SIMD[float_type, simd_width](0) + var t: SIMD[DType.bool, simd_width] = True + + for i in range(MAX_ITERS): + if not t.reduce_or(): + break + y2 = y * y + y = x.fma(y + y, cy) + t = x.fma(x, y2) <= 4 + x = x.fma(x, cx - y2) + + iters = t.select(iters + 1, iters) + + return iters +``` + +fn 키워드는 기존 파이썬 def 키워드와 비슷하지만, mojo에서의 제한 사항들을 더 엄격하게 지켜야 하는 함수를 정의하는데 쓰입니다. 디폴트 변수 ownership, 타입 명시 강제 등의 차이가 있습니다. 이렇게 code color를 통해 python으로부터의 migration을 고려한 점을 종종 발견할 수 있습니다. + +SIMD는 Single Instruction Multiple Data의 약자로, 벡터의 일종으로 이해할 수 있습니다. numpy의 1차원 ndarray, 혹은 pytorch의 1차원 tensor에 대응된다고 생각할 수 있습니다. 하나의 연산을 많은 데이터에 한꺼번에 적용하는 용도로 마련된 자료형으로, 현재 mojo에는 reduce, fma, min max 등의 연산들이 정의되어 있습니다. 이러한 내장 연산들을 사용하면 cpu/gpu arch 레벨까지 최적화된 수준의 처리를 할 수 있습니다. + +SIMD를 정의할 때 parameter로 자료형과 벡터의 크기를 전달하는 것을 볼 수 있습니다. 앞서 소개한 것 처럼, 이렇게 SIMD의 타입을 parameterize하더라도 컴파일 시간에 전부 처리되어 런타임 시간의 비효율을 최소화하여 동작하게 됩니다. + +`p = a.fma(b, c)`는 $p = a * b + c$ 를 각 원소별로 계산하는 연산입니다. 당연하게도 동일한 parameter (자료형, 길이) 를 가진 SIMD끼리 적용할 수 있습니다. reduce_or는 단순히 모든 값들을 or하여 값 하나를 반환하는 연산 (파이썬의 any()와 동일), select는 각 boolean 원소에 따라 True일때의 값, False일때의 값을 분기하여 계산해주는 연산입니다. + +코드를 천천히 살펴보면, x와 y가 각각 주어진 복소점 c의 실수와 허수 부분을 나타낸다는 것을 알 수 있고, t가 각 복소점들이 원점으로부터 떨어진 거리의 제곱, iters가 반지름 2인 원을 벗어나는데까지 걸리는 시행 횟수임을 알 수 있습니다. + +SIMD에서 정의된 연산들은 마찬가지로 [mojo docs](https://docs.modular.com/mojo/stdlib/builtin/simd.html#simd)에서 확인할 수 있습니다. + +```python +fn main(): + let t = Tensor[float_type](height, width) + + @parameter + fn worker(row: Int): + let scale_x = (max_x - min_x) / width + let scale_y = (max_y - min_y) / height + + @parameter + fn compute_vector[simd_width: Int](col: Int): + let cx = min_x + (col + iota[float_type, simd_width]()) * scale_x + let cy = min_y + row * scale_y + let c = ComplexSIMD[float_type, simd_width](cx, cy) + + t.data().simd_store[simd_width]( + row * width + col, mandelbrot_kernel_SIMD[simd_width](c) + ) + + vectorize[simd_width, compute_vector](width) + + @parameter + fn bench[simd_width: Int](): + for row in range(height): + worker(row) + + let vectorized_ms = Benchmark().run[bench[simd_width]]() / 1e6 + print("Number of threads:", num_cores()) + print("Vectorized:", vectorized_ms, "ms" + + # Parallelized + with Runtime() as rt: + @parameter + fn bench_parallel[simd_width: Int](): + parallelize[worker](rt, height, height) + let parallelized_ms = Benchmark().run[ + bench_parallel[simd_width] + ]() / 1e6 + + print("Parallelized:", parallelized_ms, "ms") + print("Parallel speedup:", vectorized_ms / parallelized_ms) +_ = t +``` + +메인 함수입니다. 차근차근 뜯어봅시다. + +#### main + +먼저, mojo 스크립트는 파이썬과 다르게 main 함수를 필요로 합니다. `mojo run file.mojo` 처럼 실행될 때 코드가 시작되는 엔트리포인트가 되며, 이는 C, C++과 비슷하다고 보면 좋을 듯 합니다. + +2차원 텐서 `t` 를 정의하여, 각 복소점의 결과값을 저장하고자 합니다. +이 `t`에 내부 함수들(worker, bench 등)이 저장해야 하는데, memory safety를 위해 ownership을 지향하는 mojo에서는 사실 구현하기 어렵습니다. 하지만 이런 경우를 대비하여 몇몇 unsafe한 기능들이 있는데, `@parameter` 데코레이터가 그 중 하나입니다. + +여기처럼 nested function (main -> worker, bench) 에서, 외부 scope에서 정의된 변수에 접근하고 싶은 경우에는 내부 함수에 @parameter를 추가하면 가능해집니다. 성능상 중요한 포인트는 아니니 가볍게 이해하고 넘어갑시다. + +#### worker + +worker 함수는 행의 위치가 주어졌을 때 (i.e. 허수 부분 y값이 고정되었을 때), 해당 복소점들의 시행 횟수를 모두 구하는 역할을 합니다. 그 내부에서 다시 compute_vector 함수를 정의하는데, 이 함수가 실제로 `simd_width` 개의 점들 가지고 위에서 정의한 `mandelbrot_kernel_SIMD` 함수를 이용하여 결과값인 t를 채우는 것을 볼 수 있습니다. +굳이 simd_width와 width를 둘다 정의하여 사용하는 이유는, 하드웨어 환경에 따라 지원되는, 혹은 효율적인 simd_width가 다를 수 있기 때문입니다. 가장 위의 alias부분을 다시 보면, 하드웨어 환경에서 지정하는 simd width를 float 타입에 대해서 가져오는 것을 확인할 수 있습니다. + +vectorize를 통해 width개의 점들에 대해, simd_width개씩 나눠서 처리하도록 구현되어 있습니다. vectorize는 단순히 SIMD를 편리하게 분할 처리하기 위한 함수로 보이고, 특별히 병렬 처리나 저수준 최적화가 지원되는 것 같지는 않습니다. for loop로 구현해도 성능상의 차이는 없어 보입니다. + +#### bench + +이렇게 구현된 worker 함수를 bench 함수에서 각 행마다 한번씩 돌려줍니다. 여기까지 특별히 병렬 처리를 하지는 않았기 때문에, 하나의 런타임이 하나의 CPU에서 동작하여 처리한 결과가 vectorized_ms 시간만큼 걸리게 됩니다. + +Benchmark 기능들을 사용하고 있는데, 이것 또한 mojo에 내장되어 지원하고 있는 기능입니다. AI 모델을 학습하거나 비교하는 등의 작업에 용이할 것으로 보입니다. + +#### bench_parallel + +한편, 이렇게 구현된 worker 함수를 parallelize 함수를 사용하면 어렵지 않게 병렬처리할 수 있습니다. 실제로 worker 함수를 호출하는 방식만 치환하여, height개의 작업을 여러 CPU에 나누어 처리하고 t에 결과를 받아오게 하면 손쉽게 처리됩니다. 이렇게 처리하면 parallelized_ms 시간만큼 걸리게 되며, 이것과 vectorized_ms와의 차이는 해당 기기의 cpu 수에 비례할 것입니다. + +한가지 주의할 점은, 여기에서 @parameter를 통해 외부 scope의 변수에 값을 저장하게 했는데, 이 점이 실수나 버그를 일으키기 쉽다는 점입니다. 병렬처리되는 작업들의 순서는 보장되지 않기 때문에, 메모리 접근 과정에서 순서가 뒤죽박죽이 되어 저장되거나, 내부 동작이 외부 값에 의존하는 경우에 잡기 어려운 버그가 생길 수 있습니다. +원칙적으로는 이러한 경우를 컴파일 시간에 배제할 수 있도록 언어를 설계하였기 때문에, (적어도 그렇게 지향하기 때문에) unsafe하지 않은 문법만을 사용하는 경우에는 이런 문제를 겪지 않게 될 것입니다. + +### PS: 세그트리 + +원래의 취지는 PS에서 자주 사용되는 코드들을 C++, Python, Mojo로 직접 구현해서 성능 차이를 직접 비교하고, Mojo가 PS에서 유의미하게 사용될 수 있을지를 검증하고 싶었습니다만, 예상했던 것보다 Mojo로 구현하고 실험하는 것이 매끄럽지 않아 성공적이지 못했습니다. + +실제로 겪은 문제들은 이렇습니다. + +- 설치 과정에서 일정 하드웨어 이상이 필요 + - 4 vCPU, 8GiB 메모리 이상이 갖춰져야 설치가 정상 동작하며, EC2 t3a.xlarge 환경에서도 cpu 점유가 과도하게 크거나 jupyter notebook에서 실행시 상당한 latency가 있었습니다. + - 코드 자체에서 측정한 CPU 시간은 무척 짧기 때문에, SDK의 성숙도 문제인 것으로 보입니다. +- 잦은 세그폴트와 오랜 응답없음 + - 재귀나 무한루프가 포함되지 않는 코드임에도 불구하고, 특정 패턴이 들어 있으면 응답이 없는 경우가 있었습니다. + - 그런 와중에 print문의 결과는 실행이 모두 완료되고 전달되는 방식이라 어느 부분이 왜 문제인지 확인하기 어려웠습니다. +- 파이썬과 interop + - 분명 함께 사용 가능한 것은 맞는데, 파이썬에서 만든 데이터를 mojo에서 가져와 활용하기가 무척 어려웠습니다. + - 기본적인 `__iter__` 등의 메소드들은 PythonObject를 통해 지원하기 때문에 이론상 가능한 것은 맞지만, 사용성이 무척 떨어져 재구현하는 것이 오히려 더 비용이 적게 들 수 있을 듯 합니다. +- 무척 적은 syntactic sugar, 다양한 벡터 자료형들 + - SIMD, Buffer, DTypePointer, VariadicList, InlinedFixedVector 등 다양한 벡터 자료형이 있는데, 이들의 활용처나 특징들을 알기 어렵고, 서로간의 전환이 아주 매끄럽지는 않았습니다. + +그래도 아카이브용으로 여기에 mojo로 간단한 세그트리를 구현하고자 했던 코드를 공유합니다. + +```python +import time +import math +import random +from utils.vector import InlinedFixedVector +from utils.static_tuple import StaticTuple + +alias NMAX = 1000000+10 +alias XMAX = 1000000000 +alias array_t = InlinedFixedVector[NMAX, Int] +alias query_t = StaticTuple[3, Int] +alias query_array_t = InlinedFixedVector[NMAX, query_t] + +# Make Data +let n: Int = 1000000 +let a: array_t = array_t(0) +a.append(0) +for i in range(n): + let value = random.random_si64(1, XMAX).to_int() + a.append(value) + +let m: Int = 1000000 +let q: query_array_t = query_array_t(0) +q.append(query_t(0, 0, 0)) +for i in range(m): + let op = random.random_si64(1, 2).to_int() + if op == 1: + let pos = random.random_si64(1, NMAX).to_int() + let val = random.random_si64(1, XMAX).to_int() + let value = query_t(op, pos, val) + q.append(value) + else: + var l = random.random_si64(1, NMAX).to_int() + var r = random.random_si64(1, NMAX).to_int() + if l > r: + l, r = r, l + let value = query_t(op, l, r) + q.append(value) + +# Solve +alias BMAX = 2**22 +alias tree_t = InlinedFixedVector[BMAX, Int] + +fn update(inout tree: tree_t, nd: Int, s: Int, e: Int, pos: Int, val: Int): + if pos < s or pos > e: + return + if s == e: + tree[nd] = val + return + let m = (s + e) // 2 + update(tree, 2 * nd, s, m, pos, val) + update(tree, 2 * nd + 1, m + 1, e, pos, val) + tree[nd] = tree[2 * nd] + tree[2 * nd + 1] + +fn query(borrowed tree: tree_t, nd: Int, s: Int, e: Int, l: Int, r: Int) -> Int: + if l > e or r < s: + return 0 + if l <= s and e <= r: + return tree[nd] + let m = (s + e) // 2 + return query(tree, 2 * nd, s, m, l, r) + query(tree, 2 * nd + 1, m + 1, e, l, r) + +fn solve( + inout answer: array_t, + borrowed n: Int, + borrowed a: array_t, + borrowed m: Int, + borrowed q: query_array_t +): + var tree = tree_t(0) + for i in range(math.min(n * 4, BMAX)): + tree.append(0) + + for idx in range(n): + let val: Int = a[idx] + update(tree, 1, 1, n, idx + 1, val) + + for i in range(m): + let op: Int = q[i][0] + let x: Int = q[i][1] + let y: Int = q[i][2] + if op == 1: + update(tree, 1, 1, n, x, y) + else: + answer.append(query(tree, 1, 1, n, x, y)) +``` + +## 감상 + +파이썬은 정말 유연하고 강력하지만, 실제 서비스에 쓰기에는 무척 제약이 많습니다. 성능, 코드 관리, 아키텍쳐 대응 등의 기저에 깔린 문제를 유기한 채 사용처를 확장하다 보니 현대 언어에서는 기본적으로 제공되는 기능들을 땜질식으로 채워나가고 있습니다. CUDA나 OpenCL 같은 서로 호환되지 않는 하드웨어 가속 프레임워크, mypy나 pylance처럼 코드의 로직 레벨까지 깊게 관여하는 linting tool, gevent나 eventlet같은 third-party concurrency 라이브러리 등이 예시라고 할 수 있겠습니다. + +물론 python 3으로의 migration, 다양한 파이썬 개선 제안(PEP) 들을 통해 여러가지 돌파구를 찾으려고 해왔지만, 그러한 시도들이 적용되는 것보다 더 나은 기술에 대한 수요는 훨씬 더 빠르게 늘어나고 있는 듯 합니다. 특히 Modular 팀이 말하는 것 처럼, 이제는 AI 기술의 발목을 잡는 것이 벗어날 수 없는 파이썬 생태계라는 것도 분명 공감하는 사람들이 많을 것입니다. + +레딧이나 Hacker News, 유튜브 등을 통해 외국 개발자들의 반응을 보면, '누군가 이 문제를 푼다면 Modular가 풀어낼 것이라고 생각하지만, 풀 수 있는 문제인지를 모르겠다' 는 평가가 있습니다. 그만큼 어려운 문제이기도 하지만, Modular 팀이 훌륭한 팀이라는 뜻이기도 하고, 또한 그런 뛰어난 사람들이 도전할 만큼 중요한 문제라는 뜻이기도 합니다. + +개인적으로 Mojo에 대해서 외부에서 드러나는 것만 봤을때는 문제를 분명 금방 풀어낼 것 같고, 유의미한 도약을 이루어냈다고 생각했는데, 실제로 사용해보니 훨씬 더 갈 길이 먼 것처럼 느껴집니다. SDK를 더 보강하고, Python에서의 migration을 더 매끄럽게 만들고, 개발자들이 mojo의 라이브러리를 만들도록 유도하고, 실제 기업이나 연구에서 사용되도록 하는데까지 얼마나 시간이 더 필요할지 잘 예상이 되지 않네요. + +하지만 아직 0.X버전이고, 착수한지 2년도 안되고, 공개된지 한달도 되지 않았기 때문에 미숙한 것은 어쩔 수 없다고 생각합니다. 파이썬을 많이 사용했고, 많이 데였던 사람으로써 여전히 응원하는 마음이 크고, 더 많은 사람들의 관심과 피드백이 반영되어 더 나은 생태계를 만들어 내기를 바라며, 실제로 그렇게 되리라고 믿습니다. + +여담이지만, 주된 타겟층이 AI 연구이기 때문에 PS에 활용하고자 한다면 시간과 노력을 들여 주의깊게 분석하고 패턴을 정립해야 할 것으로 보입니다. 하지만 속도와 문법, 그리고 파이썬 생태계 면에서 분명 이점이 있을 것이라 생각하기 때문에, 몇 년 안에는 유의미하게 사용되지 않을까 하는 예상을 던져봅니다.