From 225dbb23f3e2595e1146b92fe9d95f4807575456 Mon Sep 17 00:00:00 2001 From: Jiasheng Zhang Date: Fri, 8 Oct 2021 12:35:03 +0800 Subject: [PATCH 01/10] [Doc] Add documentation for gui system and install trouble shooting. (#2985) * [doc] Complete outline of docs in gui system * Add links to apis * Complete simple apis with links * added examples to gui drawing apis * More examples of gui API, added static/asset for example images storage * delete libtinfo trouble shoot * Added apis for single circle, line, and triangle * resolve conversation * fix typos * Auto Format * Resolve conversations * resolve conversation Co-authored-by: Taichi Gardener --- docs/lang/articles/get-started.md | 2 +- docs/lang/articles/misc/gui.md | 310 ++++++++++++++++-- docs/lang/articles/misc/install.md | 20 +- .../articles/static/assets/arrow_field.png | Bin 0 -> 12649 bytes docs/lang/articles/static/assets/arrows.png | Bin 0 -> 8589 bytes docs/lang/articles/static/assets/circles.png | Bin 0 -> 17103 bytes .../static/assets/colored_circles.png | Bin 0 -> 16649 bytes docs/lang/articles/static/assets/lines.png | Bin 0 -> 24362 bytes .../articles/static/assets/point_field.png | Bin 0 -> 9662 bytes docs/lang/articles/static/assets/rect.png | Bin 0 -> 7976 bytes .../lang/articles/static/assets/triangles.png | Bin 0 -> 9685 bytes python/taichi/misc/gui.py | 4 +- 12 files changed, 289 insertions(+), 47 deletions(-) create mode 100644 docs/lang/articles/static/assets/arrow_field.png create mode 100644 docs/lang/articles/static/assets/arrows.png create mode 100644 docs/lang/articles/static/assets/circles.png create mode 100644 docs/lang/articles/static/assets/colored_circles.png create mode 100644 docs/lang/articles/static/assets/lines.png create mode 100644 docs/lang/articles/static/assets/point_field.png create mode 100644 docs/lang/articles/static/assets/rect.png create mode 100644 docs/lang/articles/static/assets/triangles.png diff --git a/docs/lang/articles/get-started.md b/docs/lang/articles/get-started.md index 70dc913a3037f..b6d250d157f26 100644 --- a/docs/lang/articles/get-started.md +++ b/docs/lang/articles/get-started.md @@ -16,7 +16,7 @@ python3 -m pip install taichi ``` :::note -Currently, Taichi only supports Python 3.6/3.7/3.8 (64-bit). +Currently, Taichi only supports Python 3.6/3.7/3.8/3.9 (64-bit). ::: import Tabs from '@theme/Tabs'; diff --git a/docs/lang/articles/misc/gui.md b/docs/lang/articles/misc/gui.md index da421694b4ef9..fc321deb817dd 100644 --- a/docs/lang/articles/misc/gui.md +++ b/docs/lang/articles/misc/gui.md @@ -9,9 +9,10 @@ Taichi has a built-in GUI system to help users visualize results. ## Create a window -`ti.GUI(name, res)` creates a window. If `res` is scalar, then width will be equal to height. +[`ti.GUI(name, res)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=gui%20gui#taichi.misc.gui.GUI) +creates a window. -The following codes show how to create a window of resolution `640x360`: +The following code show how to create a window of resolution `640x360`: ```python gui = ti.GUI('Window Title', (640, 360)) @@ -33,7 +34,8 @@ while gui.running: ## Display a window -`gui.show(filename)` helps display a window. If `filename` is specified, a screenshot will be saved to the file specified by the name. For example, the following saves frames of the window to `.png`s: +[`gui.show(filename)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=show#taichi.misc.gui.GUI.show) +helps display a window. If `filename` is specified, a screenshot will be saved to the path. For example, the following saves frames of the window to `.png`s: for frame in range(10000): render(img) @@ -43,8 +45,29 @@ while gui.running: ## Paint on a window +Taichi's GUI supports painting simple geometric objects, such as lines, triangles, rectangles, circles, and text. -`gui.set_image(pixels)` sets an image to display on the window. +:::note + +The position parameter of every drawing API expects input of 2-element tuples, +whose values are the relative position of the object range from 0.0 to 1.0. +(0.0, 0.0) stands for the lower left corner of the window, and (1.0, 1.0) stands for the upper right corner. + +Acceptable input for positions are taichi fields or numpy arrays. Primitive arrays in python are NOT acceptable. + +For simplicity, we use numpy arrays in the examples below. + +::: + +:::tip + +For detailed API description, please click on the API code. For instance, click on +`gui.get_image()` to see the description to get a GUI images. + +::: + +[`gui.set_image(pixels)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=set_image#taichi.misc.gui.GUI.set_image) +sets an image to display on the window. The image pixels are set from the values of `img[i, j]`, where `i` indicates the horizontal coordinates (from left to right) and `j` the vertical coordinates (from bottom to top). @@ -56,8 +79,7 @@ If the window size is `(x, y)`, then `img` must be one of: - `ti.field(shape=(x, y, 2))`, where `2` is for `(r, g)` channels -- `ti.Vector.field(3, shape=(x, y))` `(r, g, b)` channels on each - component +- `ti.Vector.field(3, shape=(x, y))` `(r, g, b)` channels on each component - `ti.Vector.field(2, shape=(x, y))` `(r, g)` channels on each component @@ -79,11 +101,162 @@ The data type of `img` must be one of: - `float64`, range `[0, 1]` +:::note + +When using `float32` or `float64` as the data type, `img` entries will be clipped into range [0, 1] for display. + +::: + +[`gui.get_image()`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=get_image#taichi.misc.gui.GUI.get_image) +gets the 4-channel (RGBA) image shown in the current GUI system. + +[`gui.circle(pos)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=circle#taichi.misc.gui.GUI.circle) +draws one solid circle. + +The color and radius of circles can be further specified with additional parameters. + +[`gui.circles(pos)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=circles#taichi.misc.gui.GUI.circles) +draws solid circles. + +The color and radius of circles can be further specified with additional parameters. For a single color, use the `color` parameter. +For multiple colors, use `palette` and `palette_indices` instead. + +:::note + +The unit of raduis in GUI APIs is number of pixels. + +::: + +For examples: +```python +gui.circles(pos, radius=3, color=0x068587) +``` +draws circles all with radius of 1.5 and blue color positioned at pos array. + +![circles](../static/assets/circles.png) +```python +gui.circles(pos, radius=3, palette=[0x068587, 0xED553B, 0xEEEEF0], palette_indices=material) +``` +draws circles with radius of 1.5 and three different colors differed by `material`, an integer array with the same size as +`pos`. Each integer in `material` indicates which color the associated circle use (i.e. array [0, 1, 2] indicates these three +circles are colored separately by the first, second, and third color in `palette`. + +![circles](../static/assets/colored_circles.png) + +[`gui.line(begin, end)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=line#taichi.misc.gui.GUI.line) +draws one line. + +The color and radius of lines can be further specified with additional parameters. + +[`gui.lines(begin, end)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=line#taichi.misc.gui.GUI.lines) +draws lines. + +`begin` and `end` both require input of positions. + +The color and radius of lines can be further specified with additional parameters. + +For example: +```python +gui.lines(begin=X, end=Y, radius=2, color=0x068587) +``` +draws line segments from X positions to Y positions with width of 2 and color in light blue. + +![lines](../static/assets/lines.png) + +[`gui.triangle(a, b, c)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=triangle#taichi.misc.gui.GUI.triangle) +draws one solid triangle. + +The color of triangles can be further specified with additional parameters. + +[`gui.triangles(a, b, c)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=triangles#taichi.misc.gui.GUI.triangles) +draws solid triangles. + +The color of triangles can be further specified with additional parameters. + +For example: +```python +gui.triangles(a=X, b=Y, c=Z, color=0xED553B) +``` +draws triangles with color in red and three points positioned at X, Y, and Z. + +![triangles](../static/assets/triangles.png) + +[`gui.rect(topleft, bottomright)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=rect#taichi.misc.gui.GUI.rect) +draws a hollow rectangle. + +The color and radius of the stroke of rectangle can be further specified with additional parameters. + +For example: +```python +gui.rect([0, 0], [0.5, 0.5], radius=1, color=0xED553B) +``` +draws a rectangle of top left corner at [0, 0] and bottom right corner at [0.5, 0.5], with stroke of radius of 1 and color in red. + +![rect](../static/assets/rect.png) + +[`gui.arrows(origin, direction)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=arrows#taichi.misc.gui.GUI.arrows) +draws arrows. + +`origin` and `direction` both require input of positions. `origin` refers to the positions of arrows' origins, `direction` +refers to the directions where the arrows point to relative to their origins. + +The color and radius of arrows can be further specified with additional parameters. + +For example: +```python +x = nunpy.array([[0.1, 0.1], [0.9, 0.1]]) +y = nunpy.array([[0.3, 0.3], [-0.3, 0.3]]) +gui.arrows(x, y, radius=1, color=0xFFFFFF) +``` +draws two arrow originated at [0.1, 0.1], [0.9, 0.1] and pointing to [0.3, 0.3], [-0.3, 0.3] with radius of 1 and color in white. + +![arrows](../static/assets/arrows.png) +[`gui.arrow_field(direction)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=arrow_field#taichi.misc.gui.GUI.arrow_field) +draws a field of arrows. -## Convert RGB to Hex +The `direction` requires a field of `shape=(col, row, 2)` where `col` refers to the number of columns of arrow field and `row` +refers to the number of rows of arrow field. -`ti.rgb_to_hex(rgb)` can convert a (R, G, B) tuple of floats into a single integer value, e.g., +The color and bound of arrow field can be further specified with additional parameters. + +For example: +```python +gui.arrow_field(x, bound=0.5, color=0xFFFFFF) # x is a field of shape=(5, 5, 2) +``` +draws a 5 by 5 arrows pointing to random directions. + +![arrow_field](../static/assets/arrow_field.png) + +[`gui.point_field(radius)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=point_field#taichi.misc.gui.GUI.point_field) +draws a field of points. + +The `radius` requires a field of `shape=(col, row)` where `col` refers to the number of columns of arrow field and `row` +refers to the number of rows of arrow field. + +The color and bound of point field can be further specified with additional parameters. + +For example: +```python +x = numpy.array([[3, 5, 7, 9], [9, 7, 5, 3], [6, 6, 6, 6]]) +gui.point_field(radius=x, bound=0.5, color=0xED553B) +``` +draws a 3 by 4 point field of radius stored in the array. + +![point_field](../static/assets/point_field.png) + +[`gui.text(content, pos)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=text#taichi.misc.gui.GUI.text) +draws a line of text on screen. + +The font size and color of text can be further specified with additional parameters. + +## RGB & Hex conversion. + +[`ti.hex_to_rgb(hex)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=hex_to_rgb#taichi.misc.gui.hex_to_rgb) +can convert a single integer value to a (R, G, B) tuple of floats. + +[`ti.rgb_to_hex(rgb)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=rgb#taichi.misc.gui.rgb_to_hex) +can convert a (R, G, B) tuple of floats into a single integer value, e.g., ```python rgb = (0.4, 0.8, 1.0) @@ -96,7 +269,6 @@ hex = ti.rgb_to_hex(rgb) # np.array([0x66ccff, 0x007fff]) The return values can be used in GUI drawing APIs. - ## Event processing Every event have a key and type. @@ -136,7 +308,8 @@ gui.get_event(ti.GUI.PRESS) gui.get_event((ti.GUI.PRESS, ti.GUI.ESCAPE), (ti.GUI.RELEASE, ti.GUI.SPACE)) ``` -`gui.running` can help check the state of the window. `ti.GUI.EXIT` occurs when you click on the close (X) button of a window. +[`gui.running`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=running#taichi.misc.gui.GUI.running) +can help check the state of the window. `ti.GUI.EXIT` occurs when you click on the close (X) button of a window. `gui.running` will obtain `False` when the GUI is being closed. For example, loop until the close button is clicked: @@ -156,7 +329,8 @@ You can also close the window by manually setting `gui.running` to`False`: gui.set_image(pixels) gui.show() -`gui.get_event(a, ...)` tries to pop an event from the queue, and stores it into `gui.event`. +[`gui.get_event(a, ...)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=get_event#taichi.misc.gui.GUI.get_event) +tries to pop an event from the queue, and stores it into `gui.event`. For example: @@ -170,7 +344,8 @@ For example, loop until ESC is pressed: gui.set_image(img) gui.show() -`gui.get_events(a, ...)` is basically the same as `gui.get_event`, except that it returns a generator of events instead of storing into `gui.event`: +[`gui.get_events(a, ...)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=get_event#taichi.misc.gui.GUI.get_events) +is basically the same as `gui.get_event`, except that it returns a generator of events instead of storing into `gui.event`: for e in gui.get_events(): if e.key == ti.GUI.ESCAPE: @@ -180,7 +355,8 @@ For example, loop until ESC is pressed: elif e.key in ['a', ti.GUI.LEFT]: ... -`gui.is_pressed(key, ...)` can detect the keys you pressed. It must be used together with `gui.get_event`, or it won't be updated! For +[`gui.is_pressed(key, ...)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=is_pressed#taichi.misc.gui.GUI.is_pressed) +can detect the keys you pressed. It must be used together with `gui.get_event`, or it won't be updated! For example: while True: @@ -190,11 +366,30 @@ example: elif gui.is_pressed('d', ti.GUI.RIGHT): print('Go right!') -`gui.get_cursor_pos()` can return current cursor position within the window. For example: +:::caution + +`gui.is_pressed()` must be used together with `gui.get_event()`, or it won't be updated! + +::: + +For example: + +```python +while True: + gui.get_event() # must be called before is_pressed + if gui.is_pressed('a', ti.GUI.LEFT): + print('Go left!') + elif gui.is_pressed('d', ti.GUI.RIGHT): + print('Go right!') +``` + +[`gui.get_cursor_pos()`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=get_cursor#taichi.misc.gui.GUI.get_cursor_pos) +can return current cursor position within the window. For example: mouse_x, mouse_y = gui.get_cursor_pos() -`gui.fps_limit` sets the FPS limit for a window. For example, to cap FPS at 24, simply use `gui.fps_limit = 24`. This helps reduce the overload on your hardware especially when you're using OpenGL on your integrated GPU which could make desktop slow to response. +[`gui.fps_limit`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=fps#taichi.misc.gui.GUI.fps_limit) +sets the FPS limit for a window. For example, to cap FPS at 24, simply use `gui.fps_limit = 24`. This helps reduce the overload on your hardware especially when you're using OpenGL on your integrated GPU which could make desktop slow to response. @@ -202,22 +397,33 @@ example: Sometimes it's more intuitive to use widgets like slider or button to control the program variables instead of using chaotic keyboard bindings. Taichi GUI provides a set of widgets for that reason: -For example: +[`gui.slider(text, min, max)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=slider#taichi.misc.gui.GUI.slider) +creates a slider following the text `{text}: {value:.3f}`. - radius = gui.slider('Radius', 1, 50) +[`gui.label(text)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=label#taichi.misc.gui.GUI.label) +displays the label as: `{text}: {value:.3f}`. - while gui.running: - print('The radius now is', radius.value) - ... - radius.value += 0.01 - ... - gui.show() +[`gui.button(text)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=button#taichi.misc.gui.GUI.button) +creates a button with text on it. + +For example: +```python +radius = gui.slider('Radius', 1, 50) + +while gui.running: + print('The radius now is', radius.value) + ... + radius.value += 0.01 + ... + gui.show() +``` ## Image I/O -`ti.imwrite(img, filename)` can export a `np.ndarray` or Taichi field (`ti.Matrix.field`, `ti.Vector.field`, or `ti.field`) to a specified location `filename`. +[`ti.imwrite(img, filename)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=imwrite#taichi.misc.image.imwrite) +can export a `np.ndarray` or Taichi field (`ti.Matrix.field`, `ti.Vector.field`, or `ti.field`) to a specified location `filename`. Same as `ti.GUI.show(filename)`, the format of the exported image is determined by **the suffix of** `filename` as well. Now `ti.imwrite` supports exporting images to `png`, `img` and `jpg` and we recommend using `png`. @@ -235,7 +441,7 @@ pixels = ti.field(dtype=type, shape=shape) @ti.kernel def draw(): for i, j in pixels: - pixels[i, j] = ti.random() * 255 # integars between [0, 255] for ti.u8 + pixels[i, j] = ti.random() * 255 # integers between [0, 255] for ti.u8 draw() @@ -246,8 +452,8 @@ Besides, for RGB or RGBA images, `ti.imwrite` needs to receive a field which has Generally the value of the pixels on each channel of a `png` image is an integer in \[0, 255\]. For this reason, `ti.imwrite` will **cast fields** which has different data types all **into integers between \[0, 255\]**. As a result, `ti.imwrite` has the following requirements for different data types of input fields: -- For float-type (`ti.f16`, `ti.f32`, etc) input fields, **the value of each pixel should be float between \[0.0, 1.0\]**. Otherwise `ti.imwrite` will first clip them into \[0.0, 1.0\]. Then they are multiplied by 256 and casted to integers ranging from \[0, 255\]. -- For int-type (`ti.u8`, `ti.u16`, etc) input fields, **the value of each pixel can be any valid integer in its own bounds**. These integers in this field will be scaled to \[0, 255\] by being divided over the upper bound of its basic type accordingly. +- For float-type (`ti.f16`, `ti.f32`, etc.) input fields, **the value of each pixel should be float between \[0.0, 1.0\]**. Otherwise `ti.imwrite` will first clip them into \[0.0, 1.0\]. Then they are multiplied by 256 and cast to integers ranging from \[0, 255\]. +- For int-type (`ti.u8`, `ti.u16`, etc.) input fields, **the value of each pixel can be any valid integer in its own bounds**. These integers in this field will be scaled to \[0, 255\] by being divided over the upper bound of its basic type accordingly. Here is another example: @@ -271,3 +477,53 @@ draw() ti.imwrite(pixels, f"export_f32.png") ``` + +[`ti.imread(filename)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=imread#taichi.misc.image.imread) +loads an image from the target filename and returns it as a `np.ndarray(dtype=np.uint8)`. +Each value in this returned field is an integer in [0, 255]. + +[`ti.imshow(img, windname)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=imshow#taichi.misc.image.imshow) +creates an instance of ti.GUI and show the input image on the screen. It has the same logic as `ti.imwrite` for different data types. + +[`ti.imresize(img, w)`](https://api-docs.taichi.graphics/src/taichi.misc.html?highlight=imresize#taichi.misc.image.imresize) +resizes the img specified. + +## Zero-copying frame buffer +When the GUI resolution (window size) is large, it sometimes becomes difficult to achieve 60 FPS even without any kernel +invocations between two frames. + +This is mainly due to the copy overhead, where Taichi GUI needs to copy the image buffer from one place to another. +This process is necessary for the 2D drawing functions, such as `gui.circles`, to work. The larger the image shape is, +the larger the overhead. + +Fortunately, sometimes your program only needs `gui.set_image` alone. In such cases, you can enable the `fast_gui` option +for better performance. This mode allows Taichi GUI to directly write the image data to the frame buffer without additional +copying, resulting in a much better FPS. + +```python +gui = ti.GUI(res, title, fast_gui=True) +``` + +:::note + +Because of the zero-copying mechanism, the image passed into `gui.set_image` must already be in the display-compatible +format. That is, this field must either be a `ti.Vector(3)` (RGB) or a `ti.Vector(4)` (RGBA). In addition, each channel +must be of type `ti.f32`, `ti.f64` or `ti.u8`. + +::: + +:::note + +If possible, consider enabling this option, especially when `fullscreen=True`. + +::: + +:::caution + +Despite the performance boost, it has many limitations as trade off: + +`gui.set_image` is the only available paint API in this mode. + +`gui.set_image` will only take Taichi 3D or 4D vector fields (RGB or RGBA) as input. + +::: diff --git a/docs/lang/articles/misc/install.md b/docs/lang/articles/misc/install.md index 50ee3c14a66b5..9c06b15622ec4 100644 --- a/docs/lang/articles/misc/install.md +++ b/docs/lang/articles/misc/install.md @@ -6,20 +6,6 @@ sidebar_position: 0 ### Linux issues -- If Taichi crashes and reports `libtinfo.so.5 not found`: - - - On Ubuntu, execute `sudo apt install libtinfo-dev`. - - - On Arch Linux, first edit `/etc/pacman.conf`, and append these - lines: - - ``` - [archlinuxcn] - Server = https://mirrors.tuna.tsinghua.edu.cn/archlinuxcn/$arch - ``` - - Then execute `sudo pacman -Syy ncurses5-compat-libs`. - - If Taichi crashes and reports `` /usr/lib/libstdc++.so.6: version `CXXABI_1.3.11' not found ``: @@ -49,11 +35,11 @@ sidebar_position: 0 ERROR: No matching distribution found for taichi ``` - - Make sure you're using Python version 3.6/3.7/3.8: + - Make sure you're using Python version 3.6/3.7/3.8/3.9: ```bash python3 -c "print(__import__('sys').version[:3])" - # 3.6, 3.7 or 3.8 + # 3.6, 3.7, 3.8 or 3.9 ``` - Make sure your Python executable is 64-bit: @@ -74,7 +60,7 @@ sidebar_position: 0 [E 05/14/20 10:46:49.911] Received signal 7 (Bus error) ``` - This might be because that your NVIDIA GPU is pre-Pascal and it + This might be because that your NVIDIA GPU is pre-Pascal, and it has limited support for [Unified Memory](https://www.nextplatform.com/2019/01/24/unified-memory-the-final-piece-of-the-gpu-programming-puzzle/). diff --git a/docs/lang/articles/static/assets/arrow_field.png b/docs/lang/articles/static/assets/arrow_field.png new file mode 100644 index 0000000000000000000000000000000000000000..aec332feff18c9bf7932f110f3f823800a75c2b8 GIT binary patch literal 12649 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 E1FoorApigX literal 0 HcmV?d00001 diff --git a/docs/lang/articles/static/assets/circles.png b/docs/lang/articles/static/assets/circles.png new file mode 100644 index 0000000000000000000000000000000000000000..e6665229a49c98e48c32a8b8a21312bfda602b04 GIT binary patch literal 17103 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 YM2|hbBM7gvFz}z%j@{eSEbIgS2clHAO#lD@ literal 0 HcmV?d00001 diff --git a/docs/lang/articles/static/assets/lines.png b/docs/lang/articles/static/assets/lines.png new file mode 100644 index 0000000000000000000000000000000000000000..b64fc41927218eec258a327f9e9451a9df6c2821 GIT binary patch literal 24362 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/lang/articles/static/assets/point_field.png b/docs/lang/articles/static/assets/point_field.png new file mode 100644 index 0000000000000000000000000000000000000000..c6c185213b50d31eaab61e3c718d463cd5f652bc GIT binary patch literal 9662 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 literal 0 HcmV?d00001 diff --git a/docs/lang/articles/static/assets/rect.png b/docs/lang/articles/static/assets/rect.png new file mode 100644 index 0000000000000000000000000000000000000000..74c566248b3ccdf8a194be0f9bf6735ac5970e61 GIT binary patch literal 7976 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&t&wwUqN(1_rtJo-U3d6>-TDtcw*5q?ksD z(J&ZI2cy|wv?v%Y2}eu9(UNd@m4wXVOg)Pm?Q@(SxNdm*JHGzkU-{lk7X=oCau^sI zactV?vBuU;>d0YbmIycHq8lv?8@}*c*Sx*jAHTTJN{Y#Zi_ws8Tj;i^#Rsi(oD#M$ zN!WEt-uT8o?Vo=AAGx#h7b`mK7La(G#JBv)QIlDrZq5lNED$1%qbH1Ev1UD#FX5)$ zaM1;+h;0Iz#%?g{jyqIKOWT1AR)bkzV0@rezRZXMNm-M{t(sx+%UYQE8DR50VdjI3 zH$gIf62$m>Fylsz9}OI6${bBOquB~lOpO)_qlLm~q2LUv)<^5w(UtznQ_L=x#C6(s5em~v{^y=v{v-m`v2~m zp92+Wu{12TH<;Pm_8!;>1a>$^iP10^O$VddV6-S0EeS_U!qJj&*p-9~PZb#$9QMjz Um~%P-$YNmdboFyt=akR{0HLnvEdT%j literal 0 HcmV?d00001 diff --git a/docs/lang/articles/static/assets/triangles.png b/docs/lang/articles/static/assets/triangles.png new file mode 100644 index 0000000000000000000000000000000000000000..32ffa160105b858974941194726affd7c65b6890 GIT binary patch literal 9685 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 t!N Date: Thu, 7 Oct 2021 22:14:24 -0700 Subject: [PATCH 02/10] [ci] Copy paste linux & windows fixes from presubmit to release. (#3103) Related: #3079, #3064 --- .github/workflows/release.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3f292f7122988..98ab6258dd2fc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,10 +84,10 @@ jobs: glewinfo ti diagnose ti changelog - ti test -vr2 -t2 -k "not ndarray" + ti test -vr2 -t2 -k "not ndarray and not torch" # ndarray test might OOM if run with -t2. # FIXME: unify this with presubmit.yml to avoid further divergence - ti test -vr2 -t1 -k "ndarray" + ti test -vr2 -t1 -k "ndarray or torch" env: PYTHON: ${{ matrix.python }} @@ -267,8 +267,7 @@ jobs: 7z x taichi-llvm-10.0.0-msvc2019.zip -otaichi_llvm curl --retry 10 --retry-delay 5 https://github.com/taichi-dev/taichi_assets/releases/download/llvm10/clang-10.0.0-win.zip -LO 7z x clang-10.0.0-win.zip -otaichi_clang - $env:PATH += ";C:\taichi_llvm\bin" - $env:PATH += ";C:\taichi_clang\bin" + $env:PATH = ";C:\taichi_llvm\bin;C:\taichi_clang\bin;" + $env:PATH clang --version cd D:\a\taichi\taichi python -m pip install -r requirements_dev.txt @@ -293,8 +292,7 @@ jobs: - name: Test shell: powershell run: | - $env:PATH += ";C:\taichi_llvm\bin" - $env:PATH += ";C:\taichi_clang\bin" + $env:PATH = ";C:\taichi_llvm\bin;C:\taichi_clang\bin;" + $env:PATH python -c "import taichi" python examples/algorithm/laplace.py python bin/taichi diagnose From b6ea8a3ba39ea775b0edb0addddf8795c586dbf2 Mon Sep 17 00:00:00 2001 From: Ye Kuang Date: Fri, 8 Oct 2021 13:17:38 +0800 Subject: [PATCH 03/10] [metal] Separate runtime and snodes initialization (#3093) Co-authored-by: Taichi Gardener --- taichi/backends/metal/kernel_manager.cpp | 237 ++++++++---------- taichi/backends/metal/metal_program.cpp | 1 + .../metal/shaders/runtime_utils.metal.h | 2 +- taichi/backends/metal/struct_metal.cpp | 2 +- 4 files changed, 111 insertions(+), 131 deletions(-) diff --git a/taichi/backends/metal/kernel_manager.cpp b/taichi/backends/metal/kernel_manager.cpp index 621f514b563b6..2596c3bac85bf 100644 --- a/taichi/backends/metal/kernel_manager.cpp +++ b/taichi/backends/metal/kernel_manager.cpp @@ -261,8 +261,6 @@ class SparseRuntimeMtlKernelBase : public CompiledMtlKernelBase { class ListgenOpMtlKernel : public SparseRuntimeMtlKernelBase { public: struct Params : public SparseRuntimeMtlKernelBase::Params { - const SNodeDescriptorsMap *snode_descriptors{nullptr}; - const SNode *snode() const { return kernel_attribs->runtime_list_op_attribs->snode; } @@ -318,7 +316,6 @@ class CompiledTaichiKernel { std::string mtl_source_code; const TaichiKernelAttributes *ti_kernel_attribs; const KernelContextAttributes *ctx_attribs; - const SNodeDescriptorsMap *snode_descriptors; MTLDevice *device; MemoryPool *mem_pool; KernelProfilerBase *profiler; @@ -359,7 +356,6 @@ class CompiledTaichiKernel { kparams.device = device; kparams.mtl_func = mtl_func.get(); kparams.mem_pool = params.mem_pool; - kparams.snode_descriptors = params.snode_descriptors; kernel = std::make_unique(kparams); } else if (ktype == KernelTaskType::gc) { GcOpMtlKernel::Params kparams; @@ -570,7 +566,6 @@ class KernelManager::Impl { explicit Impl(Params params) : config_(params.config), compiled_runtime_module_(params.compiled_runtime_module), - compiled_structs_(params.compiled_structs), mem_pool_(params.mem_pool), host_result_buffer_(params.host_result_buffer), profiler_(params.profiler), @@ -583,18 +578,6 @@ class KernelManager::Impl { command_queue_ = new_command_queue(device_.get()); TI_ASSERT(command_queue_ != nullptr); create_new_command_buffer(); - if (compiled_structs_.root_size > 0) { - root_mem_ = std::make_unique( - compiled_structs_.root_size, mem_pool_); - root_buffer_ = new_mtl_buffer_no_copy(device_.get(), root_mem_->ptr(), - root_mem_->size()); - TI_ASSERT(root_buffer_ != nullptr); - buffer_meta_data_.root_buffer_size = root_mem_->size(); - TI_DEBUG("Metal root buffer size: {} bytes", root_mem_->size()); - ActionRecorder::get_instance().record( - "allocate_root_buffer", - {ActionArg("size_in_bytes", (int64)root_mem_->size())}); - } global_tmps_mem_ = std::make_unique( taichi_global_tmp_buffer_size, mem_pool_); @@ -607,7 +590,6 @@ class KernelManager::Impl { device_.get(), global_tmps_mem_->ptr(), global_tmps_mem_->size()); TI_ASSERT(global_tmps_buffer_ != nullptr); - TI_ASSERT(compiled_runtime_module_.runtime_size > 0); const size_t mem_pool_bytes = (config_->device_memory_GB * 1024 * 1024 * 1024ULL); runtime_mem_ = std::make_unique( @@ -625,7 +607,7 @@ class KernelManager::Impl { ActionRecorder::get_instance().record( "allocate_runtime_buffer", {ActionArg("runtime_buffer_size_in_bytes", (int64)runtime_mem_->size()), - ActionArg("runtime_struct_size_in_bytes", + ActionArg("runtime_size_in_bytes", (int64)compiled_runtime_module_.runtime_size), ActionArg("memory_pool_size", (int64)mem_pool_bytes)}); @@ -639,10 +621,29 @@ class KernelManager::Impl { print_mem_->size()); TI_ASSERT(print_buffer_ != nullptr); - init_runtime(params.root_id); + init_runtime_buffer(compiled_runtime_module_); clear_print_assert_buffer(); } + void add_compiled_snode_tree(const CompiledStructs &compiled_tree) { + if (compiled_tree.root_size > 0) { + root_mem_ = std::make_unique(compiled_tree.root_size, + mem_pool_); + root_buffer_ = new_mtl_buffer_no_copy(device_.get(), root_mem_->ptr(), + root_mem_->size()); + TI_ASSERT(root_buffer_ != nullptr); + buffer_meta_data_.root_buffer_size += root_mem_->size(); + TI_DEBUG("Metal root={} buffer_size={} bytes", compiled_tree.root_id, + root_mem_->size()); + ActionRecorder::get_instance().record( + "allocate_root_buffer", + {ActionArg("root_id={}", (int64)compiled_tree.root_id), + ActionArg("size_in_bytes", (int64)root_mem_->size())}); + } + + init_snode_tree_sparse_runtime(compiled_tree); + } + void register_taichi_kernel(const std::string &taichi_kernel_name, const std::string &mtl_kernel_source_code, const TaichiKernelAttributes &ti_kernel_attribs, @@ -661,7 +662,6 @@ class KernelManager::Impl { params.mtl_source_code = mtl_kernel_source_code; params.ti_kernel_attribs = &ti_kernel_attribs; params.ctx_attribs = &ctx_attribs; - params.snode_descriptors = &compiled_structs_.snode_descriptors; params.device = device_.get(); params.mem_pool = mem_pool_; params.profiler = profiler_; @@ -750,14 +750,69 @@ class KernelManager::Impl { } private: - void init_runtime(int root_id) { + void init_runtime_buffer(const CompiledRuntimeModule &rtm_module) { + char *addr = runtime_mem_->ptr(); + // init rand_seeds + // TODO(k-ye): Provide a way to use a fixed seed in dev mode. + std::mt19937 generator( + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count()); + std::uniform_int_distribution distr( + 0, std::numeric_limits::max()); + for (int i = 0; i < kNumRandSeeds; ++i) { + uint32_t *s = reinterpret_cast(addr); + *s = distr(generator); + addr += sizeof(uint32_t); + } + TI_DEBUG("Initialized random seeds size={}", rtm_module.rand_seeds_size); + using namespace shaders; - char *addr = reinterpret_cast(runtime_mem_->ptr()); + addr = runtime_mem_->ptr() + rtm_module.rand_seeds_size; const char *const addr_begin = addr; - const int max_snodes = compiled_structs_.max_snodes; - const auto &snode_descriptors = compiled_structs_.snode_descriptors; - // init snode_metas dev_runtime_mirror_.snode_metas = (SNodeMeta *)addr; + size_t addr_offset = sizeof(SNodeMeta) * kMaxNumSNodes; + addr += addr_offset; + TI_DEBUG("SNodeMeta, size={} accumulated={}", addr_offset, + (addr - addr_begin)); + dev_runtime_mirror_.snode_extractors = (SNodeExtractors *)addr; + addr_offset = sizeof(SNodeExtractors) * kMaxNumSNodes; + addr += addr_offset; + TI_DEBUG("SNodeExtractors, size={} accumulated={}", addr_offset, + (addr - addr_begin)); + dev_runtime_mirror_.snode_lists = (ListManagerData *)addr; + addr_offset = sizeof(ListManagerData) * kMaxNumSNodes; + addr += addr_offset; + TI_DEBUG("ListManagerData, size={} accumulated={}", addr_offset, + (addr - addr_begin)); + dev_runtime_mirror_.snode_allocators = (NodeManagerData *)addr; + addr_offset = sizeof(NodeManagerData) * kMaxNumSNodes; + addr += addr_offset; + TI_DEBUG("NodeManagerData, size={} accumulated={}", addr_offset, + (addr - addr_begin)); + dev_runtime_mirror_.ambient_indices = (NodeManagerData::ElemIndex *)addr; + addr_offset = sizeof(NodeManagerData::ElemIndex) * kMaxNumSNodes; + addr += addr_offset; + TI_DEBUG("SNode ambient elements, size={} accumulated={}", addr_offset, + (addr - addr_begin)); + + // Initialize the memory allocator + dev_mem_alloc_mirror_ = reinterpret_cast(addr); + // Make sure the retured memory address is always greater than 1. + dev_mem_alloc_mirror_->next = shaders::MemoryAllocator::kInitOffset; + TI_DEBUG("Memory allocator, begin={} next={}", (addr - addr_begin), + dev_mem_alloc_mirror_->next); + + mark_runtime_buffer_modified(); + } + + void init_snode_tree_sparse_runtime(const CompiledStructs &snode_tree) { + using namespace shaders; + const int max_snodes = snode_tree.max_snodes; + const auto &snode_descriptors = snode_tree.snode_descriptors; + char *addr = nullptr; + // init snode_metas + addr = (char *)dev_runtime_mirror_.snode_metas; for (int i = 0; i < max_snodes; ++i) { auto iter = snode_descriptors.find(i); if (iter == snode_descriptors.end()) { @@ -798,12 +853,8 @@ class KernelManager::Impl { i, snode_type_name(sn_meta.snode->type), rtm_meta->element_stride, rtm_meta->num_slots, rtm_meta->mem_offset_in_parent); } - size_t addr_offset = sizeof(SNodeMeta) * kMaxNumSNodes; - addr += addr_offset; - TI_DEBUG("Initialized SNodeMeta, size={} accumulated={}", addr_offset, - (addr - addr_begin)); // init snode_extractors - dev_runtime_mirror_.snode_extractors = (SNodeExtractors *)addr; + addr = (char *)dev_runtime_mirror_.snode_extractors; for (int i = 0; i < max_snodes; ++i) { auto iter = snode_descriptors.find(i); if (iter == snode_descriptors.end()) { @@ -823,12 +874,8 @@ class KernelManager::Impl { } TI_DEBUG(""); } - addr_offset = sizeof(SNodeExtractors) * kMaxNumSNodes; - addr += addr_offset; - TI_DEBUG("Initialized SNodeExtractors, size={} accumulated={}", addr_offset, - (addr - addr_begin)); // init snode_lists - dev_runtime_mirror_.snode_lists = (ListManagerData *)addr; + addr = (char *)dev_runtime_mirror_.snode_lists; ListManagerData *const rtm_list_begin = reinterpret_cast(addr); for (int i = 0; i < max_snodes; ++i) { @@ -847,12 +894,8 @@ class KernelManager::Impl { TI_DEBUG("ListManagerData\n id={}\n num_elems_per_chunk={}\n", i, num_elems_per_chunk); } - addr_offset = sizeof(ListManagerData) * kMaxNumSNodes; - addr += addr_offset; - TI_DEBUG("Initialized ListManagerData, size={} accumulated={}", addr_offset, - (addr - addr_begin)); // init snode_allocators - dev_runtime_mirror_.snode_allocators = (NodeManagerData *)addr; + addr = (char *)dev_runtime_mirror_.snode_allocators; auto init_node_mgr = [&snode_descriptors](const SNodeDescriptor &sn_desc, NodeManagerData *nm_data) { nm_data->data_list.element_stride = sn_desc.element_stride; @@ -896,51 +939,6 @@ class KernelManager::Impl { init_node_mgr(sn_desc, nm_data); snode_id_to_nodemgrs.push_back(std::make_pair(i, nm_data)); } - addr_offset = sizeof(NodeManagerData) * kMaxNumSNodes; - addr += addr_offset; - TI_DEBUG("Initialized NodeManagerData, size={} accumulated={}", addr_offset, - (addr - addr_begin)); - // ambient_indices initialization has to be delayed, because it relies on - // the initialization of MemoryAllocator. - auto *const ambient_indices_begin = - reinterpret_cast(addr); - dev_runtime_mirror_.ambient_indices = ambient_indices_begin; - addr_offset = sizeof(NodeManagerData::ElemIndex) * kMaxNumSNodes; - addr += addr_offset; - TI_DEBUG( - "Delayed the initialization of SNode ambient elements, size={} " - "accumulated={}", - addr_offset, (addr - addr_begin)); - // init rand_seeds - // TODO(k-ye): Provide a way to use a fixed seed in dev mode. - std::mt19937 generator( - std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count()); - const auto rand_seeds_begin = (addr - addr_begin); - buffer_meta_data_.randseedoffset_in_runtime_buffer = rand_seeds_begin; - std::uniform_int_distribution distr( - 0, std::numeric_limits::max()); - for (int i = 0; i < kNumRandSeeds; ++i) { - uint32_t *s = reinterpret_cast(addr); - *s = distr(generator); - addr += sizeof(uint32_t); - } - TI_DEBUG("Initialized random seeds, begin={} size={} accumuated={}", - rand_seeds_begin, kNumRandSeeds * sizeof(uint32_t), - (addr - addr_begin)); - ActionRecorder::get_instance().record( - "initialize_runtime_buffer", - { - ActionArg("rand_seeds_begin", (int64)rand_seeds_begin), - }); - - // Initialize the memory allocator - auto *mem_alloc = reinterpret_cast(addr); - // Make sure the retured memory address is always greater than 1. - mem_alloc->next = shaders::MemoryAllocator::kInitOffset; - TI_DEBUG("Initialized memory allocator, begin={} next={}", - (addr - addr_begin), mem_alloc->next); // Root list is static, so it can be initialized here once. ListgenElement root_elem; @@ -949,20 +947,25 @@ class KernelManager::Impl { root_elem.coords.at[i] = 0; } ListManager root_lm; - root_lm.lm_data = rtm_list_begin + root_id; - root_lm.mem_alloc = mem_alloc; + root_lm.lm_data = rtm_list_begin + snode_tree.root_id; + root_lm.mem_alloc = dev_mem_alloc_mirror_; root_lm.append(root_elem); // Initialize all the ambient elements + auto *const ambient_indices_begin = dev_runtime_mirror_.ambient_indices; for (const auto &p : snode_id_to_nodemgrs) { NodeManager nm; nm.nm_data = p.second; - nm.mem_alloc = mem_alloc; + nm.mem_alloc = dev_mem_alloc_mirror_; const auto snode_id = p.first; ambient_indices_begin[snode_id] = nm.allocate(); TI_DEBUG("AmbientIndex\n id={}\n mem_alloc->next={}\n", snode_id, - mem_alloc->next); + dev_mem_alloc_mirror_->next); } + mark_runtime_buffer_modified(); + } + + void mark_runtime_buffer_modified() { did_modify_range(runtime_buffer_.get(), /*location=*/0, runtime_mem_->size()); } @@ -997,35 +1000,6 @@ class KernelManager::Impl { // print_runtime_debug(); } - void print_runtime_debug() { - // If debugging is necessary, make sure this is called after - // blit_buffers_and_sync(). - int *root_base = reinterpret_cast(root_mem_->ptr()); - for (int i = 0; i < 10; ++i) { - TI_INFO("root[{}]={}", i, root_base[i]); - } - - const auto &sn_descs = compiled_structs_.snode_descriptors; - for (int i = 0; i < compiled_structs_.max_snodes; ++i) { - auto iter = sn_descs.find(i); - if (iter == sn_descs.end()) { - continue; - } - // const SNodeDescriptor &sn_desc = iter->second; - shaders::ListManager lm; - lm.lm_data = (dev_runtime_mirror_.snode_lists + i); - lm.mem_alloc = dev_mem_alloc_mirror_; - - shaders::NodeManagerData *nma = - (dev_runtime_mirror_.snode_allocators + i); - TI_INFO( - "ListManager for SNode={} num_active={} num_allocated={} " - "free_list_used={}", - i, lm.num_active(), nma->data_list.next, nma->free_list_used); - } - TI_INFO(""); - } - void check_assertion_failure() { // TODO: Copy this to program's result_buffer, and let the Taichi runtime // handle the assertion failures uniformly. @@ -1138,19 +1112,19 @@ class KernelManager::Impl { MemoryPool *const mem_pool_; uint64_t *const host_result_buffer_; KernelProfilerBase *const profiler_; - nsobj_unique_ptr device_; - nsobj_unique_ptr command_queue_; - nsobj_unique_ptr cur_command_buffer_; - std::size_t command_buffer_id_; + nsobj_unique_ptr device_{nullptr}; + nsobj_unique_ptr command_queue_{nullptr}; + nsobj_unique_ptr cur_command_buffer_{nullptr}; + std::size_t command_buffer_id_{0}; std::unique_ptr root_mem_; nsobj_unique_ptr root_buffer_; - std::unique_ptr global_tmps_mem_; - nsobj_unique_ptr global_tmps_buffer_; - std::unique_ptr runtime_mem_; - nsobj_unique_ptr runtime_buffer_; + std::unique_ptr global_tmps_mem_{nullptr}; + nsobj_unique_ptr global_tmps_buffer_{nullptr}; + std::unique_ptr runtime_mem_{nullptr}; + nsobj_unique_ptr runtime_buffer_{nullptr}; // TODO: Rename these to 'print_assert_{mem|buffer}_' - std::unique_ptr print_mem_; - nsobj_unique_ptr print_buffer_; + std::unique_ptr print_mem_{nullptr}; + nsobj_unique_ptr print_buffer_{nullptr}; std::unordered_map> compiled_taichi_kernels_; PrintStringTable print_strtable_; @@ -1176,6 +1150,10 @@ class KernelManager::Impl { TI_ERROR("Metal not supported on the current OS"); } + void add_compiled_snode_tree(const CompiledStructs &) { + TI_ERROR("Metal not supported on the current OS"); + } + void register_taichi_kernel(const std::string &taichi_kernel_name, const std::string &mtl_kernel_source_code, const TaichiKernelAttributes &ti_kernel_attribs, @@ -1217,6 +1195,7 @@ KernelManager::~KernelManager() { } void KernelManager::add_compiled_snode_tree(const CompiledStructs &snode_tree) { + impl_->add_compiled_snode_tree(snode_tree); } void KernelManager::register_taichi_kernel( diff --git a/taichi/backends/metal/metal_program.cpp b/taichi/backends/metal/metal_program.cpp index 98185879f9bf4..76ba87d173a64 100644 --- a/taichi/backends/metal/metal_program.cpp +++ b/taichi/backends/metal/metal_program.cpp @@ -56,6 +56,7 @@ void MetalProgramImpl::materialize_snode_tree( metal_kernel_mgr_ = std::make_unique(std::move(params_)); } + metal_kernel_mgr_->add_compiled_snode_tree(metal_compiled_structs_.value()); } std::unique_ptr MetalProgramImpl::make_aot_module_builder() { diff --git a/taichi/backends/metal/shaders/runtime_utils.metal.h b/taichi/backends/metal/shaders/runtime_utils.metal.h index b15c2fb0c033f..ae29a1507366d 100644 --- a/taichi/backends/metal/shaders/runtime_utils.metal.h +++ b/taichi/backends/metal/shaders/runtime_utils.metal.h @@ -27,12 +27,12 @@ // The actual Runtime struct has to be emitted by codegen, because it depends // on the number of SNodes. struct Runtime { + uint32_t *rand_seeds = nullptr; SNodeMeta *snode_metas = nullptr; SNodeExtractors *snode_extractors = nullptr; ListManagerData *snode_lists = nullptr; NodeManagerData *snode_allocators = nullptr; NodeManagerData::ElemIndex *ambient_indices = nullptr; - uint32_t *rand_seeds = nullptr; }; #define METAL_BEGIN_RUNTIME_UTILS_DEF diff --git a/taichi/backends/metal/struct_metal.cpp b/taichi/backends/metal/struct_metal.cpp index 5ecae03b6d7b0..144153fd536cc 100644 --- a/taichi/backends/metal/struct_metal.cpp +++ b/taichi/backends/metal/struct_metal.cpp @@ -364,12 +364,12 @@ class RuntimeModuleCompiler { line_appender_.append_raw(shaders::kMetalRuntimeStructsSourceCode); emit(""); emit("struct Runtime {{"); + emit(" uint32_t rand_seeds[{}];", kNumRandSeeds); emit(" SNodeMeta snode_metas[{}];", kMaxNumSNodes); emit(" SNodeExtractors snode_extractors[{}];", kMaxNumSNodes); emit(" ListManagerData snode_lists[{}];", kMaxNumSNodes); emit(" NodeManagerData snode_allocators[{}];", kMaxNumSNodes); emit(" NodeManagerData::ElemIndex ambient_indices[{}];", kMaxNumSNodes); - emit(" uint32_t rand_seeds[{}];", kNumRandSeeds); emit("}};"); emit(""); line_appender_.append_raw(shaders::kMetalRuntimeUtilsSourceCode); From 3535c648756eb559e3c8f45d912621eed175e1fc Mon Sep 17 00:00:00 2001 From: Ailing Zhang Date: Thu, 7 Oct 2021 21:27:06 -0400 Subject: [PATCH 04/10] [refactor] Stop overriding taichi.core from taichi/lang/impl.py. ghstack-source-id: 5053aa768ec9dbbe2ca00684793cc148518bf019 Pull Request resolved: https://github.com/taichi-dev/taichi/pull/3099 --- python/taichi/__init__.py | 5 ++++- python/taichi/core/__init__.py | 4 ---- python/taichi/lang/__init__.py | 3 --- python/taichi/lang/impl.py | 17 +++++++++-------- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/python/taichi/__init__.py b/python/taichi/__init__.py index 03f7b5fef83ab..555cfc2ec10ba 100644 --- a/python/taichi/__init__.py +++ b/python/taichi/__init__.py @@ -2,13 +2,16 @@ import taichi.ad as ad from taichi._logging import * -from taichi.core import * +from taichi.core import (get_os_name, package_root, require_version, + start_memory_monitoring) +from taichi.core import ti_core as core from taichi.lang import * # TODO(archibate): It's `taichi.lang.core` overriding `taichi.core` from taichi.main import main from taichi.misc import * from taichi.testing import * from taichi.tools import * from taichi.torch_io import from_torch, to_torch +from taichi.type import * import taichi.ui as ui diff --git a/python/taichi/core/__init__.py b/python/taichi/core/__init__.py index 3296b46faeca5..a19fb81c1e88e 100644 --- a/python/taichi/core/__init__.py +++ b/python/taichi/core/__init__.py @@ -1,7 +1,3 @@ from taichi.core.util import * -# TODO: move this to taichi/__init__.py. -# This is blocked since we currently require importing this before taichi.lang -# but yapf refuses to give up formatting there. -from taichi.type import * __all__ = [s for s in dir() if not s.startswith('_')] diff --git a/python/taichi/lang/__init__.py b/python/taichi/lang/__init__.py index 3368bf629c0ae..37310d5b94193 100644 --- a/python/taichi/lang/__init__.py +++ b/python/taichi/lang/__init__.py @@ -31,9 +31,6 @@ import taichi as ti -# TODO(#2223): Remove -core = _ti_core - runtime = impl.get_runtime() i = axes(0) diff --git a/python/taichi/lang/impl.py b/python/taichi/lang/impl.py index ff231de970622..a1b3b61d14690 100644 --- a/python/taichi/lang/impl.py +++ b/python/taichi/lang/impl.py @@ -19,6 +19,7 @@ python_scope, taichi_scope, to_pytorch_type) from taichi.misc.util import deprecated, get_traceback, warning from taichi.snode.fields_builder import FieldsBuilder +from taichi.type.primitive_types import f32, f64, i32, i64, u32, u64 import taichi as ti @@ -284,8 +285,8 @@ def __init__(self, kernels=None): self.global_vars = [] self.print_preprocessed = False self.experimental_real_function = False - self.default_fp = ti.f32 - self.default_ip = ti.i32 + self.default_fp = f32 + self.default_ip = i32 self.target_tape = None self.grad_replaced = False self.kernels = kernels or [] @@ -294,12 +295,12 @@ def get_num_compiled_functions(self): return len(self.compiled_functions) + len(self.compiled_grad_functions) def set_default_fp(self, fp): - assert fp in [ti.f32, ti.f64] + assert fp in [f32, f64] self.default_fp = fp default_cfg().default_fp = self.default_fp def set_default_ip(self, ip): - assert ip in [ti.i32, ti.i64] + assert ip in [i32, i64] self.default_ip = ip default_cfg().default_ip = self.default_ip @@ -389,23 +390,23 @@ def _clamp_unsigned_to_range(npty, val): def make_constant_expr(val): _taichi_skip_traceback = 1 if isinstance(val, (int, np.integer)): - if pytaichi.default_ip in {ti.i32, ti.u32}: + if pytaichi.default_ip in {i32, u32}: # It is not always correct to do such clamp without the type info on # the LHS, but at least this makes assigning constant to unsigned # int work. See https://github.com/taichi-dev/taichi/issues/2060 return Expr( _ti_core.make_const_expr_i32( _clamp_unsigned_to_range(np.int32, val))) - elif pytaichi.default_ip in {ti.i64, ti.u64}: + elif pytaichi.default_ip in {i64, u64}: return Expr( _ti_core.make_const_expr_i64( _clamp_unsigned_to_range(np.int64, val))) else: assert False elif isinstance(val, (float, np.floating, np.ndarray)): - if pytaichi.default_fp == ti.f32: + if pytaichi.default_fp == f32: return Expr(_ti_core.make_const_expr_f32(val)) - elif pytaichi.default_fp == ti.f64: + elif pytaichi.default_fp == f64: return Expr(_ti_core.make_const_expr_f64(val)) else: assert False From 4fbe06061dfc4cbdf7b902221cd635d125e82ff9 Mon Sep 17 00:00:00 2001 From: Ailing Zhang Date: Thu, 7 Oct 2021 21:27:11 -0400 Subject: [PATCH 05/10] [refactor] Remove importing outside top level. ghstack-source-id: 07016b5d9105ca2671e5b98696cd9978d64ace24 Pull Request resolved: https://github.com/taichi-dev/taichi/pull/3100 --- python/taichi/ad.py | 4 +++- python/taichi/diagnose.py | 14 +++++++------- python/taichi/lang/shell.py | 11 +++++------ python/taichi/lang/sparse_matrix.py | 9 +++++---- python/taichi/main.py | 23 ++++------------------- python/taichi/misc/util.py | 9 +++------ python/taichi/testing.py | 11 +++++------ 7 files changed, 32 insertions(+), 49 deletions(-) diff --git a/python/taichi/ad.py b/python/taichi/ad.py index ee6bba1f2011a..04a7ba61e6096 100644 --- a/python/taichi/ad.py +++ b/python/taichi/ad.py @@ -1,3 +1,6 @@ +from taichi.lang import impl + + def grad_replaced(func): """A decorator for python function to customize gradient with Taichi's autodiff system, e.g. `ti.Tape()` and `kernel.grad()`. @@ -32,7 +35,6 @@ def grad_replaced(func): >>> multiply_grad(a)""" def decorated(*args, **kwargs): # TODO [#3025]: get rid of circular imports and move this to the top. - from taichi.lang import impl impl.get_runtime().grad_replaced = True if impl.get_runtime().target_tape: impl.get_runtime().target_tape.insert(decorated, args) diff --git a/python/taichi/diagnose.py b/python/taichi/diagnose.py index 7c3449290257a..184e508e06da0 100644 --- a/python/taichi/diagnose.py +++ b/python/taichi/diagnose.py @@ -1,13 +1,13 @@ +import locale +import os +import platform +import subprocess +import sys + + def main(): print('Taichi system diagnose:') print('') - - import locale - import os - import platform - import subprocess - import sys - executable = sys.executable print(f'python: {sys.version}') diff --git a/python/taichi/lang/shell.py b/python/taichi/lang/shell.py index f004db1e921d7..c21262917edaa 100644 --- a/python/taichi/lang/shell.py +++ b/python/taichi/lang/shell.py @@ -3,18 +3,17 @@ import os import sys +from taichi._logging import info, warn from taichi.core.util import ti_core as _ti_core -import taichi as ti - try: import sourceinspect as oinspect except ImportError: - ti.warn('`sourceinspect` not installed!') - ti.warn( + warn('`sourceinspect` not installed!') + warn( 'Without this package Taichi may not function well in Python IDLE interactive shell, ' 'Blender scripting module and Python native shell.') - ti.warn('Please run `python3 -m pip install sourceinspect` to install.') + warn('Please run `python3 -m pip install sourceinspect` to install.') import inspect as oinspect pybuf_enabled = False @@ -32,7 +31,7 @@ def _shell_pop_print(old_call): # zero-overhead! return old_call - ti.info('Graphical python shell detected, using wrapped sys.stdout') + info('Graphical python shell detected, using wrapped sys.stdout') @functools.wraps(old_call) def new_call(*args, **kwargs): diff --git a/python/taichi/lang/sparse_matrix.py b/python/taichi/lang/sparse_matrix.py index bacc9b71bd126..496c4be62c165 100644 --- a/python/taichi/lang/sparse_matrix.py +++ b/python/taichi/lang/sparse_matrix.py @@ -1,9 +1,13 @@ +import numpy as np +from taichi.core.util import ti_core as _ti_core +from taichi.lang.field import Field + + class SparseMatrix: def __init__(self, n=None, m=None, sm=None): if sm is None: self.n = n self.m = m if m else n - from taichi.core.util import ti_core as _ti_core self.matrix = _ti_core.create_sparse_matrix(n, m) else: self.n = sm.num_rows() @@ -39,8 +43,6 @@ def transpose(self): return SparseMatrix(sm=sm) def __matmul__(self, other): - import numpy as np - from taichi.lang import Field if isinstance(other, SparseMatrix): assert self.m == other.n, f"Dimension mismatch between sparse matrices ({self.n}, {self.m}) and ({other.n}, {other.m})" sm = self.matrix.matmul(other.matrix) @@ -71,7 +73,6 @@ def __init__(self, num_rows=None, num_cols=None, max_num_triplets=0): self.num_rows = num_rows self.num_cols = num_cols if num_cols else num_rows if num_rows is not None: - from taichi.core.util import ti_core as _ti_core self.ptr = _ti_core.create_sparse_matrix_builder( num_rows, num_cols, max_num_triplets) diff --git a/python/taichi/main.py b/python/taichi/main.py index e81dedb676cf8..1b616b5862792 100644 --- a/python/taichi/main.py +++ b/python/taichi/main.py @@ -4,12 +4,15 @@ import random import runpy import shutil +import subprocess import sys -import time +import timeit from collections import defaultdict from functools import wraps from pathlib import Path +import numpy as np +import pytest from colorama import Back, Fore, Style from taichi.core import ti_core as _ti_core from taichi.tools import video @@ -19,8 +22,6 @@ def timer(func): """Function decorator to benchmark a function runnign time.""" - import timeit - @wraps(func) def wrapper(*args, **kwargs): start = timeit.default_timer() @@ -113,13 +114,11 @@ def _usage(self) -> str: def _exec_python_file(filename: str): """Execute a Python file based on filename.""" # TODO: do we really need this? - import subprocess subprocess.call([sys.executable, filename] + sys.argv[1:]) @staticmethod def _get_examples_dir() -> Path: """Get the path to the examples directory.""" - import taichi as ti root_dir = ti.package_root() examples_dir = Path(root_dir) / 'examples' @@ -499,7 +498,6 @@ def get_dats(dir): def plot_in_gui(scatter): import numpy as np - import taichi as ti gui = ti.GUI('Regression Test', (640, 480), 0x001122) print('[Hint] press SPACE to go for next display') for key, data in scatter.items(): @@ -561,12 +559,10 @@ def plot_in_gui(scatter): @staticmethod def _get_benchmark_baseline_dir(): - import taichi as ti return os.path.join(_ti_core.get_repo_dir(), 'benchmarks', 'baseline') @staticmethod def _get_benchmark_output_dir(): - import taichi as ti return os.path.join(_ti_core.get_repo_dir(), 'benchmarks', 'output') @register @@ -602,7 +598,6 @@ def baseline(self, arguments: list = sys.argv[2:]): # Short circuit for testing if self.test_mode: return args - import shutil baseline_dir = TaichiMain._get_benchmark_baseline_dir() output_dir = TaichiMain._get_benchmark_output_dir() shutil.rmtree(baseline_dir, True) @@ -612,9 +607,7 @@ def baseline(self, arguments: list = sys.argv[2:]): @staticmethod def _test_python(args): print("\nRunning Python tests...\n") - import pytest - import taichi as ti root_dir = ti.package_root() test_dir = os.path.join(root_dir, 'tests') pytest_args = [] @@ -676,8 +669,6 @@ def _test_python(args): @staticmethod def _test_cpp(args): - import taichi as ti - # Cpp tests use the legacy non LLVM backend ti.reset() print("Running C++ tests...") @@ -720,7 +711,6 @@ def benchmark(self, arguments: list = sys.argv[2:]): # Short circuit for testing if self.test_mode: return args - import shutil commit_hash = _ti_core.get_commit_hash() with os.popen('git rev-parse HEAD') as f: current_commit_hash = f.read().strip() @@ -981,12 +971,7 @@ def repl(self, arguments: list = sys.argv[2:]): args = parser.parse_args(arguments) def local_scope(): - import math - import time - - import numpy as np - import taichi as ti try: import IPython IPython.embed() diff --git a/python/taichi/misc/util.py b/python/taichi/misc/util.py index 905acbdf9d9ff..21e81253c3028 100644 --- a/python/taichi/misc/util.py +++ b/python/taichi/misc/util.py @@ -1,12 +1,13 @@ import copy -import inspect +import functools +import subprocess import sys import traceback from colorama import Fore, Style from taichi.core import ti_core as _ti_core -import taichi +import taichi as ti def config_from_dict(args): @@ -133,8 +134,6 @@ def deprecated(old, new, warning_type=DeprecationWarning): Returns: Decorated fuction with warning message """ - import functools - def decorator(foo): @functools.wraps(foo) def wrapped(*args, **kwargs): @@ -225,7 +224,6 @@ def dump_dot(filepath=None, rankdir=None, embed_states_threshold=0): def dot_to_pdf(dot, filepath): assert filepath.endswith('.pdf') - import subprocess p = subprocess.Popen(['dot', '-Tpdf'], stdin=subprocess.PIPE, stdout=subprocess.PIPE) @@ -239,7 +237,6 @@ def get_kernel_stats(): def print_async_stats(include_kernel_profiler=False): - import taichi as ti if include_kernel_profiler: ti.print_kernel_profile_info() print() diff --git a/python/taichi/testing.py b/python/taichi/testing.py index 4e83bc17a9572..4cde7627e2780 100644 --- a/python/taichi/testing.py +++ b/python/taichi/testing.py @@ -1,6 +1,10 @@ import copy +import functools import itertools +import os +from tempfile import mkstemp +import pytest from taichi.core import ti_core as _ti_core import taichi as ti @@ -21,8 +25,6 @@ def get_rel_eps(): def approx(expected, **kwargs): '''Tweaked pytest.approx for OpenGL low precisions''' - import pytest - class boolean_integer: def __init__(self, value): self.value = value @@ -48,8 +50,7 @@ def allclose(x, y, **kwargs): def make_temp_file(*args, **kwargs): '''Create a temporary file''' - import os - from tempfile import mkstemp + fd, name = mkstemp(*args, **kwargs) os.close(fd) return name @@ -110,8 +111,6 @@ def test(arch=None, exclude=None, require=None, **options): return lambda x: print('No supported arch found. Skipping') def decorator(foo): - import functools - @functools.wraps(foo) def wrapped(*args, **kwargs): arch_params_sets = [arch, *_test_features.values()] From 122abcd5620abbb30f94e166fcd09c99d581944f Mon Sep 17 00:00:00 2001 From: Ailing Zhang Date: Thu, 7 Oct 2021 21:27:17 -0400 Subject: [PATCH 06/10] [refactor] Work around some cyclic imports. ghstack-source-id: 86679b90d055abbd5cbfa1908bce6d708f5cea6f Pull Request resolved: https://github.com/taichi-dev/taichi/pull/3101 --- python/taichi/lang/__init__.py | 39 +++++++++----------- python/taichi/lang/{random.py => _random.py} | 4 +- python/taichi/lang/ast/checkers.py | 7 ++-- python/taichi/lang/ast/transformer.py | 6 +-- python/taichi/lang/ast_builder_utils.py | 2 +- python/taichi/lang/expr.py | 2 +- python/taichi/lang/field.py | 4 +- python/taichi/lang/kernel_arguments.py | 2 +- python/taichi/lang/kernel_impl.py | 8 ++-- python/taichi/lang/linalg.py | 15 ++++---- python/taichi/lang/matrix.py | 4 +- python/taichi/lang/ndarray.py | 7 ++-- python/taichi/lang/sparse_solver.py | 8 ++-- python/taichi/lang/stmt_builder.py | 2 +- python/taichi/lang/types.py | 10 ++--- 15 files changed, 58 insertions(+), 62 deletions(-) rename python/taichi/lang/{random.py => _random.py} (87%) diff --git a/python/taichi/lang/__init__.py b/python/taichi/lang/__init__.py index 37310d5b94193..4c6aa7a3cae5f 100644 --- a/python/taichi/lang/__init__.py +++ b/python/taichi/lang/__init__.py @@ -1,7 +1,13 @@ +import atexit import functools import os +import shutil +import tempfile +import time from copy import deepcopy as _deepcopy +import taichi.lang.linalg +import taichi.lang.meta from taichi.core.util import locale_encode from taichi.core.util import ti_core as _ti_core from taichi.lang import impl, types @@ -298,10 +304,7 @@ def prepare_sandbox(): Returns a temporary directory, which will be automatically deleted on exit. It may contain the taichi_core shared object or some misc. files. ''' - import atexit - import shutil - from tempfile import mkdtemp - tmp_dir = mkdtemp(prefix='taichi-') + tmp_dir = tempfile.mkdtemp(prefix='taichi-') atexit.register(shutil.rmtree, tmp_dir) print(f'[Taichi] preparing sandbox at {tmp_dir}') os.mkdir(os.path.join(tmp_dir, 'runtime/')) @@ -517,8 +520,7 @@ def polar_decompose(A, dt=None): """ if dt is None: dt = impl.get_runtime().default_fp - from .linalg import polar_decompose - return polar_decompose(A, dt) + return taichi.lang.linalg.polar_decompose(A, dt) def svd(A, dt=None): @@ -536,8 +538,7 @@ def svd(A, dt=None): """ if dt is None: dt = impl.get_runtime().default_fp - from .linalg import svd - return svd(A, dt) + return taichi.lang.linalg.svd(A, dt) def eig(A, dt=None): @@ -556,9 +557,8 @@ def eig(A, dt=None): """ if dt is None: dt = impl.get_runtime().default_fp - from taichi.lang import linalg if A.n == 2: - return linalg.eig2x2(A, dt) + return taichi.lang.linalg.eig2x2(A, dt) raise Exception("Eigen solver only supports 2D matrices.") @@ -579,9 +579,8 @@ def sym_eig(A, dt=None): assert all(A == A.transpose()), "A needs to be symmetric" if dt is None: dt = impl.get_runtime().default_fp - from taichi.lang import linalg if A.n == 2: - return linalg.sym_eig2x2(A, dt) + return taichi.lang.linalg.sym_eig2x2(A, dt) raise Exception("Symmetric eigen solver only supports 2D matrices.") @@ -598,7 +597,7 @@ def randn(dt=None): """ if dt is None: dt = impl.get_runtime().default_fp - from .random import randn + from taichi.lang._random import randn return randn(dt) @@ -646,8 +645,7 @@ def Tape(loss, clear_gradients=True): if clear_gradients: clear_all_gradients() - from taichi.lang.meta import clear_loss - clear_loss(loss) + taichi.lang.meta.clear_loss(loss) return runtime.get_tape(loss) @@ -668,8 +666,7 @@ def visit(node): places = tuple(places) if places: - from taichi.lang.meta import clear_gradients - clear_gradients(places) + taichi.lang.meta.clear_gradients(places) for root_fb in FieldsBuilder.finalized_roots(): visit(root_fb) @@ -682,8 +679,6 @@ def deactivate_all_snodes(): def benchmark(func, repeat=300, args=()): - import time - def run_benchmark(): compile_time = time.time() func(*args) # compile the kernel first @@ -737,8 +732,8 @@ def benchmark_plot(fn=None, bar_distance=0, left_margin=0, size=(12, 8)): - import matplotlib.pyplot as plt - import yaml + import matplotlib.pyplot as plt # pylint: disable=C0415 + import yaml # pylint: disable=C0415 if fn is None: fn = os.path.join(_ti_core.get_repo_dir(), 'benchmarks', 'output', 'benchmark.yml') @@ -857,7 +852,7 @@ def benchmark_plot(fn=None, def stat_write(key, value): - import yaml + import yaml # pylint: disable=C0415 case_name = os.environ.get('TI_CURRENT_BENCHMARK') if case_name is None: return diff --git a/python/taichi/lang/random.py b/python/taichi/lang/_random.py similarity index 87% rename from python/taichi/lang/random.py rename to python/taichi/lang/_random.py index 6d3c5a46368d8..af92b772083d8 100644 --- a/python/taichi/lang/random.py +++ b/python/taichi/lang/_random.py @@ -1,9 +1,11 @@ import math +from taichi.lang.kernel_impl import func + import taichi as ti -@ti.func +@func def randn(dt): ''' Generates a random number from standard normal distribution diff --git a/python/taichi/lang/ast/checkers.py b/python/taichi/lang/ast/checkers.py index f3dfb292c63cb..fb8a3d62ba418 100644 --- a/python/taichi/lang/ast/checkers.py +++ b/python/taichi/lang/ast/checkers.py @@ -1,5 +1,6 @@ import ast +import taichi.lang.kernel_impl from taichi.lang.shell import oinspect @@ -68,8 +69,7 @@ def generic_visit(self, node): return if not (self.top_level or self.current_scope.allows_more_stmt): - import taichi as ti - raise ti.KernelDefError( + raise taichi.lang.kernel_impl.KernelDefError( f'No more statements allowed, at {self.get_error_location(node)}' ) old_top_level = self.top_level @@ -96,8 +96,7 @@ def visit_For(self, node): is_static = False if not (self.top_level or self.current_scope.allows_for_loop or is_static): - import taichi as ti - raise ti.KernelDefError( + raise taichi.lang.kernel_impl.KernelDefError( f'No more for loops allowed, at {self.get_error_location(node)}' ) diff --git a/python/taichi/lang/ast/transformer.py b/python/taichi/lang/ast/transformer.py index 9e83ffbbee046..01ac896f4a04f 100644 --- a/python/taichi/lang/ast/transformer.py +++ b/python/taichi/lang/ast/transformer.py @@ -1,8 +1,11 @@ import ast +import astor from taichi.lang import impl from taichi.lang.ast.symbol_resolver import ASTResolver +from taichi.lang.ast_builder_utils import BuilderContext from taichi.lang.exception import TaichiSyntaxError +from taichi.lang.stmt_builder import build_stmt import taichi as ti @@ -26,12 +29,9 @@ def print_ast(tree, title=None): return if title is not None: ti.info(f'{title}:') - import astor print(astor.to_source(tree.body[0], indent_with=' ')) def visit(self, tree): - from taichi.lang.ast_builder_utils import BuilderContext - from taichi.lang.stmt_builder import build_stmt self.print_ast(tree, 'Initial AST') ctx = BuilderContext(func=self.func, excluded_parameters=self.excluded_parameters, diff --git a/python/taichi/lang/ast_builder_utils.py b/python/taichi/lang/ast_builder_utils.py index d8b563932e5c0..103bc3a4fd368 100644 --- a/python/taichi/lang/ast_builder_utils.py +++ b/python/taichi/lang/ast_builder_utils.py @@ -8,7 +8,7 @@ def __call__(self, ctx, node): method = getattr(self, 'build_' + node.__class__.__name__, None) if method is None: try: - import astpretty + import astpretty # pylint: disable=C0415 error_msg = f'Unsupported node {node}:\n{astpretty.pformat(node)}' except: error_msg = f'Unsupported node {node}' diff --git a/python/taichi/lang/expr.py b/python/taichi/lang/expr.py index 93093f6ffb3db..0f9b14957cc5b 100644 --- a/python/taichi/lang/expr.py +++ b/python/taichi/lang/expr.py @@ -1,3 +1,4 @@ +import numpy as np from taichi.core.util import ti_core as _ti_core from taichi.lang import impl from taichi.lang.common_ops import TaichiOperations @@ -25,7 +26,6 @@ def __init__(self, *args, tb=None): # assume to be constant arg = args[0] try: - import numpy as np if isinstance(arg, np.ndarray): arg = arg.dtype(arg) except: diff --git a/python/taichi/lang/field.py b/python/taichi/lang/field.py index 1cbffe75ef813..9d480e39bc0f6 100644 --- a/python/taichi/lang/field.py +++ b/python/taichi/lang/field.py @@ -230,7 +230,7 @@ def fill(self, val): def to_numpy(self, dtype=None): if dtype is None: dtype = to_numpy_type(self.dtype) - import numpy as np + import numpy as np # pylint: disable=C0415 arr = np.zeros(shape=self.shape, dtype=dtype) from taichi.lang.meta import tensor_to_ext_arr tensor_to_ext_arr(self, arr) @@ -239,7 +239,7 @@ def to_numpy(self, dtype=None): @python_scope def to_torch(self, device=None): - import torch + import torch # pylint: disable=C0415 arr = torch.zeros(size=self.shape, dtype=to_pytorch_type(self.dtype), device=device) diff --git a/python/taichi/lang/kernel_arguments.py b/python/taichi/lang/kernel_arguments.py index a2029160324d9..0db45890a719f 100644 --- a/python/taichi/lang/kernel_arguments.py +++ b/python/taichi/lang/kernel_arguments.py @@ -2,6 +2,7 @@ from taichi.lang.any_array import AnyArray from taichi.lang.enums import Layout from taichi.lang.expr import Expr +from taichi.lang.ndarray import ScalarNdarray from taichi.lang.snode import SNode from taichi.lang.sparse_matrix import SparseMatrixBuilder from taichi.lang.util import cook_dtype, to_taichi_type @@ -47,7 +48,6 @@ def __init__(self, element_dim=None, layout=None): def extract(self, x): from taichi.lang.matrix import MatrixNdarray, VectorNdarray - from taichi.lang.ndarray import ScalarNdarray if isinstance(x, ScalarNdarray): self.check_element_dim(x, 0) return x.dtype, len(x.shape), (), Layout.AOS diff --git a/python/taichi/lang/kernel_impl.py b/python/taichi/lang/kernel_impl.py index 54ccdba44b507..86ef8508615f5 100644 --- a/python/taichi/lang/kernel_impl.py +++ b/python/taichi/lang/kernel_impl.py @@ -19,6 +19,9 @@ import taichi as ti +if util.has_pytorch(): + import torch + def _remove_indent(lines): lines = lines.split('\n') @@ -492,7 +495,6 @@ def func__(*args): if isinstance(v, Ndarray): v = v.arr has_external_arrays = True - has_torch = util.has_pytorch() is_numpy = isinstance(v, np.ndarray) if is_numpy: tmp = np.ascontiguousarray(v) @@ -509,8 +511,7 @@ def call_back(): return call_back - assert has_torch - import torch + assert util.has_pytorch() assert isinstance(v, torch.Tensor) tmp = v taichi_arch = self.runtime.prog.config.arch @@ -577,7 +578,6 @@ def call_back(): def match_ext_arr(self, v): has_array = isinstance(v, np.ndarray) if not has_array and util.has_pytorch(): - import torch has_array = isinstance(v, torch.Tensor) return has_array diff --git a/python/taichi/lang/linalg.py b/python/taichi/lang/linalg.py index b8590409ac0e0..a3572f6b067d5 100644 --- a/python/taichi/lang/linalg.py +++ b/python/taichi/lang/linalg.py @@ -1,10 +1,11 @@ from taichi.core.util import ti_core as _ti_core from taichi.lang.impl import expr_init +from taichi.lang.kernel_impl import func import taichi as ti -@ti.func +@func def polar_decompose2d(A, dt): """Perform polar decomposition (A=UP) for 2x2 matrix. @@ -25,7 +26,7 @@ def polar_decompose2d(A, dt): return r, r.transpose() @ A -@ti.func +@func def polar_decompose3d(A, dt): """Perform polar decomposition (A=UP) for 3x3 matrix. @@ -43,7 +44,7 @@ def polar_decompose3d(A, dt): # https://www.seas.upenn.edu/~cffjiang/research/svd/svd.pdf -@ti.func +@func def svd2d(A, dt): """Perform singular value decomposition (A=USV^T) for 2x2 matrix. @@ -126,7 +127,7 @@ def svd3d(A, dt, iters=None): return U, sigma, V -@ti.func +@func def eig2x2(A, dt): """Compute the eigenvalues and right eigenvectors (Av=lambda v) of a 2x2 real matrix. @@ -176,7 +177,7 @@ def eig2x2(A, dt): return eigenvalues, eigenvectors -@ti.func +@func def sym_eig2x2(A, dt): """Compute the eigenvalues and right eigenvectors (Av=lambda v) of a 2x2 real symmetric matrix. @@ -212,7 +213,7 @@ def sym_eig2x2(A, dt): return eigenvalues, eigenvectors -@ti.func +@func def svd(A, dt): """Perform singular value decomposition (A=USV^T) for arbitrary size matrix. @@ -236,7 +237,7 @@ def svd(A, dt): raise Exception("SVD only supports 2D and 3D matrices.") -@ti.func +@func def polar_decompose(A, dt): """Perform polar decomposition (A=UP) for arbitrary size matrix. diff --git a/python/taichi/lang/matrix.py b/python/taichi/lang/matrix.py index a814ca11af858..510e626df0944 100644 --- a/python/taichi/lang/matrix.py +++ b/python/taichi/lang/matrix.py @@ -1306,7 +1306,7 @@ def to_numpy(self, keep_dims=False, as_vector=None, dtype=None): dtype = to_numpy_type(self.dtype) as_vector = self.m == 1 and not keep_dims shape_ext = (self.n, ) if as_vector else (self.n, self.m) - import numpy as np + import numpy as np # pylint: disable=C0415 arr = np.zeros(self.shape + shape_ext, dtype=dtype) from taichi.lang.meta import matrix_to_ext_arr matrix_to_ext_arr(self, arr, as_vector) @@ -1324,7 +1324,7 @@ def to_torch(self, device=None, keep_dims=False): Returns: torch.tensor: The result torch tensor. """ - import torch + import torch # pylint: disable=C0415 as_vector = self.m == 1 and not keep_dims shape_ext = (self.n, ) if as_vector else (self.n, self.m) arr = torch.empty(self.shape + shape_ext, diff --git a/python/taichi/lang/ndarray.py b/python/taichi/lang/ndarray.py index a902cc4b800d3..f0a59a564cdbb 100644 --- a/python/taichi/lang/ndarray.py +++ b/python/taichi/lang/ndarray.py @@ -1,9 +1,13 @@ +import numpy as np from taichi.core.util import ti_core as _ti_core from taichi.lang import impl from taichi.lang.enums import Layout from taichi.lang.util import (cook_dtype, has_pytorch, python_scope, to_pytorch_type, to_taichi_type) +if has_pytorch(): + import torch + class Ndarray: """Taichi ndarray class implemented with a torch tensor. @@ -15,7 +19,6 @@ class Ndarray: def __init__(self, dtype, shape): assert has_pytorch( ), "PyTorch must be available if you want to create a Taichi ndarray." - import torch self.arr = torch.zeros(shape, dtype=to_pytorch_type(cook_dtype(dtype))) if impl.current_cfg().arch == _ti_core.Arch.cuda: self.arr = self.arr.cuda() @@ -94,14 +97,12 @@ def from_numpy(self, arr): Args: arr (numpy.ndarray): The source numpy array. """ - import numpy as np if not isinstance(arr, np.ndarray): raise TypeError(f"{np.ndarray} expected, but {type(arr)} provided") if tuple(self.arr.shape) != tuple(arr.shape): raise ValueError( f"Mismatch shape: {tuple(self.arr.shape)} expected, but {tuple(arr.shape)} provided" ) - import torch self.arr = torch.from_numpy(arr).to(self.arr.dtype) diff --git a/python/taichi/lang/sparse_solver.py b/python/taichi/lang/sparse_solver.py index 225faa58594e6..2ef9cdce75483 100644 --- a/python/taichi/lang/sparse_solver.py +++ b/python/taichi/lang/sparse_solver.py @@ -1,3 +1,7 @@ +import numpy as np +from taichi.core.util import ti_core as _ti_core +from taichi.lang import Field +from taichi.lang.impl import get_runtime from taichi.lang.sparse_matrix import SparseMatrix @@ -6,8 +10,6 @@ def __init__(self, solver_type="LLT", ordering="AMD"): solver_type_list = ["LLT", "LDLT", "LU"] solver_ordering = ['AMD', 'COLAMD'] if solver_type in solver_type_list and ordering in solver_ordering: - from taichi.core.util import ti_core as _ti_core - from taichi.lang.impl import get_runtime taichi_arch = get_runtime().prog.config.arch assert taichi_arch == _ti_core.Arch.x64 or taichi_arch == _ti_core.Arch.arm64, "SparseSolver only supports CPU for now." self.solver = _ti_core.make_sparse_solver(solver_type, ordering) @@ -37,8 +39,6 @@ def factorize(self, sparse_matrix): self.type_assert(sparse_matrix) def solve(self, b): - import numpy as np - from taichi.lang import Field if isinstance(b, Field): return self.solver.solve(b.to_numpy()) elif isinstance(b, np.ndarray): diff --git a/python/taichi/lang/stmt_builder.py b/python/taichi/lang/stmt_builder.py index 8fedd738d9ce8..99518c38b1231 100644 --- a/python/taichi/lang/stmt_builder.py +++ b/python/taichi/lang/stmt_builder.py @@ -1,6 +1,7 @@ import ast import copy +import astor from taichi.lang import impl from taichi.lang.ast.symbol_resolver import ASTResolver from taichi.lang.ast_builder_utils import * @@ -84,7 +85,6 @@ def build_Assert(ctx, node): raise ValueError( f"assert info must be constant, not {ast.dump(node.msg)}") else: - import astor msg = astor.to_source(node.test) node.test = build_expr(ctx, node.test) diff --git a/python/taichi/lang/types.py b/python/taichi/lang/types.py index b5e23fb7802f1..1299e378b43f5 100644 --- a/python/taichi/lang/types.py +++ b/python/taichi/lang/types.py @@ -1,5 +1,6 @@ import numbers +import taichi.lang.matrix from taichi.lang.exception import TaichiSyntaxError @@ -19,15 +20,12 @@ def field(self, **kwargs): def matrix(m, n, dtype=None): - from taichi.lang.matrix import MatrixType - return MatrixType(m, n, dtype=dtype) + return taichi.lang.matrix.MatrixType(m, n, dtype=dtype) def vector(m, dtype=None): - from taichi.lang.matrix import MatrixType - return MatrixType(m, 1, dtype=dtype) + return taichi.lang.matrix.MatrixType(m, 1, dtype=dtype) def struct(**kwargs): - from taichi.lang.struct import StructType - return StructType(**kwargs) + return taichi.lang.struct.StructType(**kwargs) From b701736fd4a06e5320c0e73f5103ce9fe6a989b8 Mon Sep 17 00:00:00 2001 From: yihong Date: Fri, 8 Oct 2021 13:35:17 +0800 Subject: [PATCH 07/10] [Lang] [bug] Fix support property decorator for data_oriented class (#3052) * fix: #3019 support @property for data_oriented class * fix: test in oop decorator property * fix: format * fix: format * Auto Format * fix: test decorator Co-authored-by: Taichi Gardener --- python/taichi/lang/kernel_impl.py | 15 ++++++++++++--- tests/python/test_oop.py | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/python/taichi/lang/kernel_impl.py b/python/taichi/lang/kernel_impl.py index 86ef8508615f5..b1555eb1f800a 100644 --- a/python/taichi/lang/kernel_impl.py +++ b/python/taichi/lang/kernel_impl.py @@ -762,9 +762,14 @@ def data_oriented(cls): Returns: The decorated class. """ - def getattr(self, item): + def _getattr(self, item): _taichi_skip_traceback = 1 - x = super(cls, self).__getattribute__(item) + method = getattr(cls, item, None) + is_property = method.__class__ == property + if is_property: + x = method.fget + else: + x = super(cls, self).__getattribute__(item) if hasattr(x, '_is_wrapped_kernel'): if inspect.ismethod(x): wrapped = x.__func__ @@ -774,10 +779,14 @@ def getattr(self, item): if wrapped._is_classkernel: ret = _BoundedDifferentiableMethod(self, wrapped) ret.__name__ = wrapped.__name__ + if is_property: + return ret() return ret + if is_property: + return x(self) return x - cls.__getattribute__ = getattr + cls.__getattribute__ = _getattr cls._data_oriented = True return cls diff --git a/tests/python/test_oop.py b/tests/python/test_oop.py index 6cab7ebad8e8b..1ce87defd5a03 100644 --- a/tests/python/test_oop.py +++ b/tests/python/test_oop.py @@ -222,3 +222,22 @@ def hook(x: ti.template()): for i in range(32): for j in range(32): assert (solver.val[i, j] == 1.0) + + +@ti.test() +def test_oop_with_portery_decorator(): + @ti.data_oriented + class TestPortery: + @property + @ti.kernel + def kernel_property(self) -> ti.i32: + return 42 + + @property + def raw_proterty(self): + return 3 + + a = TestPortery() + assert a.kernel_property == 42 + + assert a.raw_proterty == 3 From 4908d5bd21afdc1392e333a146efedd0f0127018 Mon Sep 17 00:00:00 2001 From: Xuanwo Date: Fri, 8 Oct 2021 14:08:52 +0800 Subject: [PATCH 08/10] [GUI] Add context manager support for ui.Gui (#3055) * [python] Add context manager support for ui.Gui Signed-off-by: Xuanwo * Add args for __exit__ Signed-off-by: Xuanwo * Fix the wrong usage of __enter__ Signed-off-by: Xuanwo * Rename to sub_window Signed-off-by: Xuanwo * Update comments Signed-off-by: Xuanwo * Update comments for sub_window Signed-off-by: Xuanwo * Fix comment style Signed-off-by: Xuanwo --- python/taichi/ui/gui.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/python/taichi/ui/gui.py b/python/taichi/ui/gui.py index 9f9812f465445..7cce99fad4d09 100644 --- a/python/taichi/ui/gui.py +++ b/python/taichi/ui/gui.py @@ -1,4 +1,5 @@ import pathlib +from contextlib import contextmanager from taichi.core import ti_core as _ti_core from taichi.lang.impl import default_cfg @@ -13,6 +14,30 @@ class Gui: def __init__(self, gui) -> None: self.gui = gui #reference to a PyGui + @contextmanager + def sub_window(self, name, x, y, width, height): + """Creating a context manager for subwindow + + Note: + All args of this method should align with `begin`. + + Args: + x (float): The x-coordinate (between 0 and 1) of the top-left corner of the subwindow, relative to the full window. + y (float): The y-coordinate (between 0 and 1) of the top-left corner of the subwindow, relative to the full window. + width (float): The width of the subwindow relative to the full window. + height (float): The height of the subwindow relative to the full window. + + Usage:: + + >>> with gui.sub_window(name, x, y, width, height) as g: + >>> g.text("Hello, World!") + """ + self.begin(name, x, y, width, height) + try: + yield self + finally: + self.end() + def begin(self, name, x, y, width, height): """Creates a subwindow that holds imgui widgets. From ed5d8994d831119c90a1f242a3158a22f02c98ae Mon Sep 17 00:00:00 2001 From: Ye Kuang Date: Fri, 8 Oct 2021 15:16:53 +0800 Subject: [PATCH 09/10] [metal] Rearrange how KernelManager is initialized (#3109) --- taichi/backends/metal/kernel_manager.cpp | 1 + taichi/backends/metal/kernel_manager.h | 4 +--- taichi/backends/metal/metal_program.cpp | 20 +++++++++----------- taichi/backends/metal/metal_program.h | 5 ++++- taichi/program/program_impl.h | 1 + 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/taichi/backends/metal/kernel_manager.cpp b/taichi/backends/metal/kernel_manager.cpp index 2596c3bac85bf..909eb7fdeef72 100644 --- a/taichi/backends/metal/kernel_manager.cpp +++ b/taichi/backends/metal/kernel_manager.cpp @@ -1122,6 +1122,7 @@ class KernelManager::Impl { nsobj_unique_ptr global_tmps_buffer_{nullptr}; std::unique_ptr runtime_mem_{nullptr}; nsobj_unique_ptr runtime_buffer_{nullptr}; + int last_snode_id_used_in_runtime_{-1}; // TODO: Rename these to 'print_assert_{mem|buffer}_' std::unique_ptr print_mem_{nullptr}; nsobj_unique_ptr print_buffer_{nullptr}; diff --git a/taichi/backends/metal/kernel_manager.h b/taichi/backends/metal/kernel_manager.h index 9098685bf9731..75f82bb8e7288 100644 --- a/taichi/backends/metal/kernel_manager.h +++ b/taichi/backends/metal/kernel_manager.h @@ -27,12 +27,10 @@ class KernelManager { public: struct Params { CompiledRuntimeModule compiled_runtime_module; - CompiledStructs compiled_structs; CompileConfig *config; - MemoryPool *mem_pool; uint64_t *host_result_buffer; + MemoryPool *mem_pool; KernelProfilerBase *profiler; - int root_id; }; explicit KernelManager(Params params); diff --git a/taichi/backends/metal/metal_program.cpp b/taichi/backends/metal/metal_program.cpp index 76ba87d173a64..58f519f9bdde6 100644 --- a/taichi/backends/metal/metal_program.cpp +++ b/taichi/backends/metal/metal_program.cpp @@ -29,11 +29,18 @@ void MetalProgramImpl::materialize_runtime(MemoryPool *memory_pool, KernelProfilerBase *profiler, uint64 **result_buffer_ptr) { TI_ASSERT(*result_buffer_ptr == nullptr); + TI_ASSERT(metal_kernel_mgr_ == nullptr); *result_buffer_ptr = (uint64 *)memory_pool->allocate( sizeof(uint64) * taichi_result_buffer_entries, 8); - params_.mem_pool = memory_pool; - params_.profiler = profiler; compiled_runtime_module_ = metal::compile_runtime_module(); + + metal::KernelManager::Params params; + params.compiled_runtime_module = compiled_runtime_module_.value(); + params.config = config; + params.host_result_buffer = *result_buffer_ptr; + params.mem_pool = memory_pool; + params.profiler = profiler; + metal_kernel_mgr_ = std::make_unique(std::move(params)); } void MetalProgramImpl::materialize_snode_tree( @@ -47,15 +54,6 @@ void MetalProgramImpl::materialize_snode_tree( auto *const root = tree->root(); metal_compiled_structs_ = metal::compile_structs(*root); - if (metal_kernel_mgr_ == nullptr) { - params_.compiled_structs = metal_compiled_structs_.value(); - params_.compiled_runtime_module = compiled_runtime_module_.value(); - params_.config = config; - params_.host_result_buffer = result_buffer; - params_.root_id = root->id; - metal_kernel_mgr_ = - std::make_unique(std::move(params_)); - } metal_kernel_mgr_->add_compiled_snode_tree(metal_compiled_structs_.value()); } diff --git a/taichi/backends/metal/metal_program.h b/taichi/backends/metal/metal_program.h index a3e616dd851fb..9fa9d18ff1569 100644 --- a/taichi/backends/metal/metal_program.h +++ b/taichi/backends/metal/metal_program.h @@ -1,4 +1,7 @@ #pragma once + +#include + #include "taichi/backends/metal/kernel_manager.h" #include "taichi/backends/metal/struct_metal.h" #include "taichi/system/memory_pool.h" @@ -47,8 +50,8 @@ class MetalProgramImpl : public ProgramImpl { std::optional compiled_runtime_module_{ std::nullopt}; std::optional metal_compiled_structs_{std::nullopt}; + std::vector compiled_snode_trees_; std::unique_ptr metal_kernel_mgr_{nullptr}; - metal::KernelManager::Params params_; }; } // namespace lang diff --git a/taichi/program/program_impl.h b/taichi/program/program_impl.h index 3451842b240aa..0ae9183586d1a 100644 --- a/taichi/program/program_impl.h +++ b/taichi/program/program_impl.h @@ -9,6 +9,7 @@ namespace taichi { namespace lang { + class ProgramImpl { public: // TODO: Make it safer, we exposed it for now as it's directly accessed From 43f874644f210ac2c088cfe683f6d5bf7c348b41 Mon Sep 17 00:00:00 2001 From: Ce Gao Date: Fri, 8 Oct 2021 16:38:00 +0800 Subject: [PATCH 10/10] [Lang] Support chained assignments (#3062) * feat(assign): Support chained assignments * fix: Fix the format * feat(assign): Add test cases Signed-off-by: Ce Gao * fix: Refactor table-driven style tests Signed-off-by: Ce Gao * feat(assign): Make unpack assignment a separate func Signed-off-by: Ce Gao * fix: Fix format Signed-off-by: Ce Gao * feat(assign): Refactor var creation and assign logic * fix: Fix formatting issues * feat(assign): Reuse the basic func * feat: Add comment --- python/taichi/lang/stmt_builder.py | 165 +++++++++++++++-------------- tests/python/test_assign.py | 59 +++++++++++ 2 files changed, 145 insertions(+), 79 deletions(-) create mode 100644 tests/python/test_assign.py diff --git a/python/taichi/lang/stmt_builder.py b/python/taichi/lang/stmt_builder.py index 99518c38b1231..60308cf4f424a 100644 --- a/python/taichi/lang/stmt_builder.py +++ b/python/taichi/lang/stmt_builder.py @@ -97,7 +97,6 @@ def build_Assert(ctx, node): @staticmethod def build_Assign(ctx, node): - assert (len(node.targets) == 1) node.value = build_expr(ctx, node.value) node.targets = build_exprs(ctx, node.targets) @@ -107,85 +106,93 @@ def build_Assign(ctx, node): if is_static_assign: return node - if isinstance(node.targets[0], ast.Tuple): - targets = node.targets[0].elts - - # Create - stmts = [] - - holder = parse_stmt('__tmp_tuple = ti.expr_init_list(0, ' - f'{len(targets)})') - holder.value.args[0] = node.value - - stmts.append(holder) - - def tuple_indexed(i): - indexing = parse_stmt('__tmp_tuple[0]') - StmtBuilder.set_subscript_index(indexing.value, - parse_expr("{}".format(i))) - return indexing.value - - for i, target in enumerate(targets): - is_local = isinstance(target, ast.Name) - if is_local and ctx.is_creation(target.id): - var_name = target.id - target.ctx = ast.Store() - # Create, no AST resolution needed - init = ast.Attribute(value=ast.Name(id='ti', - ctx=ast.Load()), - attr='expr_init', - ctx=ast.Load()) - rhs = ast.Call( - func=init, - args=[tuple_indexed(i)], - keywords=[], - ) - ctx.create_variable(var_name) - stmts.append( - ast.Assign(targets=[target], - value=rhs, - type_comment=None)) - else: - # Assign - target.ctx = ast.Load() - func = ast.Attribute(value=target, - attr='assign', - ctx=ast.Load()) - call = ast.Call(func=func, - args=[tuple_indexed(i)], - keywords=[]) - stmts.append(ast.Expr(value=call)) - - for stmt in stmts: - ast.copy_location(stmt, node) - stmts.append(parse_stmt('del __tmp_tuple')) - return StmtBuilder.make_single_statement(stmts) - else: - is_local = isinstance(node.targets[0], ast.Name) - if is_local and ctx.is_creation(node.targets[0].id): - var_name = node.targets[0].id - # Create, no AST resolution needed - init = ast.Attribute(value=ast.Name(id='ti', ctx=ast.Load()), - attr='expr_init', - ctx=ast.Load()) - rhs = ast.Call( - func=init, - args=[node.value], - keywords=[], - ) - ctx.create_variable(var_name) - return ast.copy_location( - ast.Assign(targets=node.targets, - value=rhs, - type_comment=None), node) + # Keep all generated assign statements and compose single one at last. + # The variable is introduced to support chained assignments. + # Ref https://github.com/taichi-dev/taichi/issues/2659. + assign_stmts = [] + for node_target in node.targets: + if isinstance(node_target, ast.Tuple): + assign_stmts.append( + StmtBuilder.build_assign_unpack(ctx, node, node_target)) else: - # Assign - node.targets[0].ctx = ast.Load() - func = ast.Attribute(value=node.targets[0], - attr='assign', - ctx=ast.Load()) - call = ast.Call(func=func, args=[node.value], keywords=[]) - return ast.copy_location(ast.Expr(value=call), node) + assign_stmts.append( + StmtBuilder.build_assign_basic(ctx, node, node_target, + node.value)) + return StmtBuilder.make_single_statement(assign_stmts) + + @staticmethod + def build_assign_unpack(ctx, node, node_target): + """Build the unpack assignments like this: (target1, target2) = (value1, value2). + The function should be called only if the node target is a tuple. + + Args: + ctx (ast_builder_utils.BuilderContext): The builder context. + node (ast.Assign): An assignment. targets is a list of nodes, + and value is a single node. + node_target (ast.Tuple): A list or tuple object. elts holds a + list of nodes representing the elements. + """ + + targets = node_target.elts + + # Create + stmts = [] + + # Create a temp list and keep values in it, delete it after the initialization is finished. + holder = parse_stmt('__tmp_tuple = ti.expr_init_list(0, ' + f'{len(targets)})') + holder.value.args[0] = node.value + + stmts.append(holder) + + def tuple_indexed(i): + indexing = parse_stmt('__tmp_tuple[0]') + StmtBuilder.set_subscript_index(indexing.value, parse_expr(f"{i}")) + return indexing.value + + # Generate assign statements for every target, then merge them into one. + for i, target in enumerate(targets): + stmts.append( + StmtBuilder.build_assign_basic(ctx, node, target, + tuple_indexed(i))) + stmts.append(parse_stmt('del __tmp_tuple')) + return StmtBuilder.make_single_statement(stmts) + + @staticmethod + def build_assign_basic(ctx, node, target, value): + """Build basic assginment like this: target = value. + + Args: + ctx (ast_builder_utils.BuilderContext): The builder context. + node (ast.Assign): An assignment. targets is a list of nodes, + and value is a single node. + target (ast.Name): A variable name. id holds the name as + a string. + value: A node representing the value. + """ + is_local = isinstance(target, ast.Name) + if is_local and ctx.is_creation(target.id): + var_name = target.id + target.ctx = ast.Store() + # Create, no AST resolution needed + init = ast.Attribute(value=ast.Name(id='ti', ctx=ast.Load()), + attr='expr_init', + ctx=ast.Load()) + rhs = ast.Call( + func=init, + args=[value], + keywords=[], + ) + ctx.create_variable(var_name) + return ast.copy_location( + ast.Assign(targets=[target], value=rhs, type_comment=None), + node) + else: + # Assign + target.ctx = ast.Load() + func = ast.Attribute(value=target, attr='assign', ctx=ast.Load()) + call = ast.Call(func=func, args=[value], keywords=[]) + return ast.copy_location(ast.Expr(value=call), node) @staticmethod def build_Try(ctx, node): diff --git a/tests/python/test_assign.py b/tests/python/test_assign.py new file mode 100644 index 0000000000000..d0297c1f63e64 --- /dev/null +++ b/tests/python/test_assign.py @@ -0,0 +1,59 @@ +import pytest + +import taichi as ti + + +@ti.test(debug=True) +def test_assign_basic(): + @ti.kernel + def func_basic(): + a = 1 + assert a == 1 + + func_basic() + + +@ti.test(debug=True) +def test_assign_unpack(): + @ti.kernel + def func_unpack(): + (a, b) = (1, 2) + assert a == 1 + assert b == 2 + + func_unpack() + + +@ti.test(debug=True) +def test_assign_chained(): + @ti.kernel + def func_chained(): + a = b = 1 + assert a == 1 + assert b == 1 + + func_chained() + + +@ti.test(debug=True) +def test_assign_chained_unpack(): + @ti.kernel + def func_chained_unpack(): + (a, b) = (c, d) = (1, 2) + assert a == 1 + assert b == 2 + assert c == 1 + assert d == 2 + + func_chained_unpack() + + +@ti.test(debug=True) +def test_assign_assign(): + @ti.kernel + def func_assign(): + a = 0 + a = 1 + assert a == 1 + + func_assign()