| |
| .. DO NOT EDIT. THIS FILE WAS AUTOMATICALLY GENERATED BY |
| .. TVM'S MONKEY-PATCHED VERSION OF SPHINX-GALLERY. TO MAKE |
| .. CHANGES, EDIT THE SOURCE PYTHON FILE: |
| .. "how_to/deploy_models/deploy_model_on_adreno.py" |
| |
| .. only:: html |
| |
| .. note:: |
| :class: sphx-glr-download-link-note |
| |
| This tutorial can be used interactively with Google Colab! You can also click |
| :ref:`here <sphx_glr_download_how_to_deploy_models_deploy_model_on_adreno.py>` to run the Jupyter notebook locally. |
| |
| .. image:: https://raw.githubusercontent.com/tlc-pack/web-data/main/images/utilities/colab_button.svg |
| :align: center |
| :target: https://colab.research.google.com/github/apache/tvm-site/blob/asf-site/docs/_downloads/b9e7311d8c56eb6e6aca08f0be35ff03/deploy_model_on_adreno.ipynb |
| :width: 300px |
| |
| .. rst-class:: sphx-glr-example-title |
| |
| .. _sphx_glr_how_to_deploy_models_deploy_model_on_adreno.py: |
| |
| |
| .. _tutorial-deploy-model-on-adreno: |
| |
| Deploy the Pretrained Model on Adreno™ |
| ====================================== |
| **Author**: Daniil Barinov, Siva Rama Krishna |
| |
| This article is a step-by-step tutorial to deploy pretrained Pytorch ResNet-18 model on Adreno (on different precisions). |
| |
| For us to begin with, PyTorch must be installed. |
| TorchVision is also required since we will be using it as our model zoo. |
| |
| A quick solution is to install it via pip: |
| |
| .. code-block:: bash |
| |
| %%shell |
| pip install torch |
| pip install torchvision |
| |
| Besides that, you should have TVM builded for Android. |
| See the following instructions on how to build it. |
| |
| `Deploy to Adreno GPU <https://tvm.apache.org/docs/how_to/deploy/adreno.html>`_ |
| |
| After the build section there should be two files in *build* directory «libtvm_runtime.so» and «tvm_rpc». |
| Let's push them to the device and run TVM RPC Server. |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 48-123 |
| |
| TVM RPC Server |
| -------------- |
| To get the hash of the device use: |
| |
| .. code-block:: bash |
| |
| adb devices |
| |
| Set the android device to use, if you have several devices connected to your computer. |
| |
| .. code-block:: bash |
| |
| export ANDROID_SERIAL=<device-hash> |
| |
| Then to upload these two files to the device you should use: |
| |
| .. code-block:: bash |
| |
| adb push {libtvm_runtime.so,tvm_rpc} /data/local/tmp |
| |
| At this moment you will have «libtvm_runtime.so» and «tvm_rpc» on path /data/local/tmp on your device. |
| Sometimes cmake can’t find «libc++_shared.so». Use: |
| |
| .. code-block:: bash |
| |
| find ${ANDROID_NDK_HOME} -name libc++_shared.so |
| |
| to find it and also push it with adb on the desired device: |
| |
| .. code-block:: bash |
| |
| adb push libc++_shared.so /data/local/tmp |
| |
| We are now ready to run the TVM RPC Server. |
| Launch rpc_tracker with following line in 1st console: |
| |
| .. code-block:: bash |
| |
| python3 -m tvm.exec.rpc_tracker --port 9190 |
| |
| Then we need to run tvm_rpc server from under the desired device in 2nd console: |
| |
| .. code-block:: bash |
| |
| adb reverse tcp:9190 tcp:9190 |
| adb forward tcp:5000 tcp:5000 |
| adb forward tcp:5002 tcp:5001 |
| adb forward tcp:5003 tcp:5002 |
| adb forward tcp:5004 tcp:5003 |
| adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/tvm_rpc server --host=0.0.0.0 --port=5000 --tracker=127.0.0.1:9190 --key=android --port-end=5100 |
| |
| Before proceeding to compile and infer model, specify TVM_TRACKER_HOST and TVM_TRACKER_PORT |
| |
| .. code-block:: bash |
| |
| export TVM_TRACKER_HOST=0.0.0.0 |
| export TVM_TRACKER_PORT=9190 |
| |
| check that the tracker is running and the device is available |
| |
| .. code-block:: bash |
| |
| python -m tvm.exec.query_rpc_tracker --port 9190 |
| |
| For example, if we have 1 Android device, |
| the output can be: |
| |
| .. code-block:: bash |
| |
| Queue Status |
| ---------------------------------- |
| key total free pending |
| ---------------------------------- |
| android 1 1 0 |
| ---------------------------------- |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 125-127 |
| |
| Configuration |
| ------------- |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 127-178 |
| |
| .. code-block:: default |
| |
| |
| import os |
| import torch |
| import torchvision |
| import tvm |
| from tvm import te |
| from tvm import relay, rpc |
| from tvm.contrib import utils, ndk |
| from tvm.contrib import graph_executor |
| from tvm.relay.op.contrib import clml |
| from tvm import autotvm |
| |
| # Below are set of configuration that controls the behaviour of this script like |
| # local run or device run, target definitions, dtype setting and auto tuning enablement. |
| # Change these settings as needed if required. |
| |
| # Adreno devices are efficient with float16 compared to float32 |
| # Given the expected output doesn't effect by lowering precision |
| # it's advisable to use lower precision. |
| # We have a helper API to make the precision conversion simple and |
| # it supports dtype with "float16" and "float16_acc32" modes. |
| # Let's choose "float16" for calculation and "float32" for accumulation. |
| |
| calculation_dtype = "float16" |
| acc_dtype = "float32" |
| |
| # Specify Adreno target before compiling to generate texture |
| # leveraging kernels and get all the benefits of textures |
| # Note: This generated example running on our x86 server for demonstration. |
| # If running it on the Android device, we need to |
| # specify its instruction set. Set :code:`local_demo` to False if you want |
| # to run this tutorial with a real device over rpc. |
| local_demo = True |
| |
| # by default on CPU target will execute. |
| # select 'cpu', 'opencl' and 'opencl -device=adreno' |
| test_target = "cpu" |
| |
| # Change target configuration. |
| # Run `adb shell cat /proc/cpuinfo` to find the arch. |
| arch = "arm64" |
| target = tvm.target.Target("llvm -mtriple=%s-linux-android" % arch) |
| |
| # Auto tuning is compute intensive and time taking task, |
| # hence disabling for default run. Please enable it if required. |
| is_tuning = False |
| tune_log = "adreno-resnet18.log" |
| |
| # To enable OpenCLML accelerated operator library. |
| enable_clml = False |
| |
| |
| |
| |
| |
| |
| |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 179-182 |
| |
| Get a PyTorch Model |
| ------------------- |
| Get resnet18 from torchvision models |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 182-191 |
| |
| .. code-block:: default |
| |
| model_name = "resnet18" |
| model = getattr(torchvision.models, model_name)(pretrained=True) |
| model = model.eval() |
| |
| # We grab the TorchScripted model via tracing |
| input_shape = [1, 3, 224, 224] |
| input_data = torch.randn(input_shape) |
| scripted_model = torch.jit.trace(model, input_data).eval() |
| |
| |
| |
| |
| |
| .. rst-class:: sphx-glr-script-out |
| |
| .. code-block:: none |
| |
| /venv/apache-tvm-py3.8/lib/python3.8/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead. |
| warnings.warn( |
| /venv/apache-tvm-py3.8/lib/python3.8/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=ResNet18_Weights.IMAGENET1K_V1`. You can also use `weights=ResNet18_Weights.DEFAULT` to get the most up-to-date weights. |
| warnings.warn(msg) |
| |
| |
| |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 192-195 |
| |
| Load a test image |
| ----------------- |
| As an example we would use classical cat image from ImageNet |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 195-221 |
| |
| .. code-block:: default |
| |
| |
| from PIL import Image |
| from tvm.contrib.download import download_testdata |
| from matplotlib import pyplot as plt |
| import numpy as np |
| |
| img_url = "https://github.com/dmlc/mxnet.js/blob/main/data/cat.png?raw=true" |
| img_path = download_testdata(img_url, "cat.png", module="data") |
| img = Image.open(img_path).resize((224, 224)) |
| plt.imshow(img) |
| plt.show() |
| |
| # Preprocess the image and convert to tensor |
| from torchvision import transforms |
| |
| my_preprocess = transforms.Compose( |
| [ |
| transforms.Resize(256), |
| transforms.CenterCrop(224), |
| transforms.ToTensor(), |
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), |
| ] |
| ) |
| img = my_preprocess(img) |
| img = np.expand_dims(img, 0) |
| |
| |
| |
| |
| .. image-sg:: /how_to/deploy_models/images/sphx_glr_deploy_model_on_adreno_001.png |
| :alt: deploy model on adreno |
| :srcset: /how_to/deploy_models/images/sphx_glr_deploy_model_on_adreno_001.png |
| :class: sphx-glr-single-img |
| |
| |
| |
| |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 222-227 |
| |
| Convert PyTorch model to Relay module |
| ------------------------------------- |
| TVM has frontend api for various frameworks under relay.frontend and now |
| for pytorch model import we have relay.frontend.from_pytorch api. |
| Input name can be arbitrary |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 227-232 |
| |
| .. code-block:: default |
| |
| input_name = "input0" |
| shape_list = [(input_name, img.shape)] |
| |
| mod, params = relay.frontend.from_pytorch(scripted_model, shape_list) |
| |
| |
| |
| |
| |
| .. rst-class:: sphx-glr-script-out |
| |
| .. code-block:: none |
| |
| /workspace/python/tvm/relay/frontend/pytorch_utils.py:47: DeprecationWarning: distutils Version classes are deprecated. Use packaging.version instead. |
| return LooseVersion(torch_ver) > ver |
| /venv/apache-tvm-py3.8/lib/python3.8/site-packages/setuptools/_distutils/version.py:346: DeprecationWarning: distutils Version classes are deprecated. Use packaging.version instead. |
| other = LooseVersion(other) |
| |
| |
| |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 233-235 |
| |
| Precisions |
| ---------- |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 235-260 |
| |
| .. code-block:: default |
| |
| |
| # Adreno devices are efficient with float16 compared to float32 |
| # Given the expected output doesn't effect by lowering precision |
| # it's advisable to use lower precision. |
| |
| # TVM support Mixed Precision through ToMixedPrecision transformation pass. |
| # We may need to register precision rules like precision type, accumultation |
| # datatype ...etc. for the required operators to override the default settings. |
| # The below helper api simplifies the precision conversions across the module. |
| |
| # Calculation dtype is set to "float16" and accumulation dtype is set to "float32" |
| # in configuration section above. |
| |
| from tvm.driver.tvmc.transform import apply_graph_transforms |
| |
| mod = apply_graph_transforms( |
| mod, |
| { |
| "mixed_precision": True, |
| "mixed_precision_ops": ["nn.conv2d", "nn.dense"], |
| "mixed_precision_calculation_type": calculation_dtype, |
| "mixed_precision_acc_type": acc_dtype, |
| }, |
| ) |
| |
| |
| |
| |
| |
| |
| |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 261-264 |
| |
| As you can see in the IR, the architecture now contains cast operations, which are |
| needed to convert to FP16 precision. |
| You can also use "float16" or "float32" precisions as other dtype options. |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 266-268 |
| |
| Prepare TVM Target |
| ------------------ |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 268-280 |
| |
| .. code-block:: default |
| |
| |
| # This generated example running on our x86 server for demonstration. |
| |
| # To deply and tun on real target over RPC please set :code:`local_demo` to False in above configuration sestion. |
| # Also, :code:`test_target` is set to :code:`llvm` as this example to make compatible for x86 demonstration. |
| # Please change it to :code:`opencl` or :code:`opencl -device=adreno` for RPC target in configuration above. |
| |
| if local_demo: |
| target = tvm.target.Target("llvm") |
| elif test_target.find("opencl"): |
| target = tvm.target.Target(test_target, host=target) |
| |
| |
| |
| |
| |
| |
| |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 281-284 |
| |
| AutoTuning |
| ---------- |
| The below few instructions can auto tune the relay module with xgboost being the tuner algorithm. |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 284-377 |
| |
| .. code-block:: default |
| |
| |
| # Auto Tuning process involces stages of extracting the tasks, defining tuning congiguration and |
| # tuning each task for best performing kernel configuration. |
| |
| # Get RPC related settings. |
| rpc_tracker_host = os.environ.get("TVM_TRACKER_HOST", "127.0.0.1") |
| rpc_tracker_port = int(os.environ.get("TVM_TRACKER_PORT", 9190)) |
| key = "android" |
| |
| # Auto tuning is compute intensive and time taking task. |
| # It is set to False in above configuration as this script runs in x86 for demonstration. |
| # Please to set :code:`is_tuning` to True to enable auto tuning. |
| |
| if is_tuning: |
| # Auto Tuning Stage 1: Extract tunable tasks |
| tasks = autotvm.task.extract_from_program( |
| mod, target=test_target, target_host=target, params=params |
| ) |
| |
| # Auto Tuning Stage 2: Define tuning configuration |
| tmp_log_file = tune_log + ".tmp" |
| measure_option = autotvm.measure_option( |
| builder=autotvm.LocalBuilder( |
| build_func=ndk.create_shared, timeout=15 |
| ), # Build the test kernel locally |
| runner=autotvm.RPCRunner( # The runner would be on a remote device. |
| key, # RPC Key |
| host=rpc_tracker_host, # Tracker host |
| port=int(rpc_tracker_port), # Tracker port |
| number=3, # Number of runs before averaging |
| timeout=600, # RPC Timeout |
| ), |
| ) |
| n_trial = 1024 # Number of iteration of training before choosing the best kernel config |
| early_stopping = False # Can be enabled to stop tuning while the loss is not minimizing. |
| |
| # Auto Tuning Stage 3: Iterate through the tasks and tune. |
| from tvm.autotvm.tuner import XGBTuner |
| |
| for i, tsk in enumerate(reversed(tasks[:3])): |
| print("Task:", tsk) |
| prefix = "[Task %2d/%2d] " % (i + 1, len(tasks)) |
| |
| # choose tuner |
| tuner = "xgb" |
| |
| # create tuner |
| if tuner == "xgb": |
| tuner_obj = XGBTuner(tsk, loss_type="reg") |
| elif tuner == "xgb_knob": |
| tuner_obj = XGBTuner(tsk, loss_type="reg", feature_type="knob") |
| elif tuner == "xgb_itervar": |
| tuner_obj = XGBTuner(tsk, loss_type="reg", feature_type="itervar") |
| elif tuner == "xgb_curve": |
| tuner_obj = XGBTuner(tsk, loss_type="reg", feature_type="curve") |
| elif tuner == "xgb_rank": |
| tuner_obj = XGBTuner(tsk, loss_type="rank") |
| elif tuner == "xgb_rank_knob": |
| tuner_obj = XGBTuner(tsk, loss_type="rank", feature_type="knob") |
| elif tuner == "xgb_rank_itervar": |
| tuner_obj = XGBTuner(tsk, loss_type="rank", feature_type="itervar") |
| elif tuner == "xgb_rank_curve": |
| tuner_obj = XGBTuner(tsk, loss_type="rank", feature_type="curve") |
| elif tuner == "xgb_rank_binary": |
| tuner_obj = XGBTuner(tsk, loss_type="rank-binary") |
| elif tuner == "xgb_rank_binary_knob": |
| tuner_obj = XGBTuner(tsk, loss_type="rank-binary", feature_type="knob") |
| elif tuner == "xgb_rank_binary_itervar": |
| tuner_obj = XGBTuner(tsk, loss_type="rank-binary", feature_type="itervar") |
| elif tuner == "xgb_rank_binary_curve": |
| tuner_obj = XGBTuner(tsk, loss_type="rank-binary", feature_type="curve") |
| elif tuner == "ga": |
| tuner_obj = GATuner(tsk, pop_size=50) |
| elif tuner == "random": |
| tuner_obj = RandomTuner(tsk) |
| elif tuner == "gridsearch": |
| tuner_obj = GridSearchTuner(tsk) |
| else: |
| raise ValueError("Invalid tuner: " + tuner) |
| |
| tsk_trial = min(n_trial, len(tsk.config_space)) |
| tuner_obj.tune( |
| n_trial=tsk_trial, |
| early_stopping=early_stopping, |
| measure_option=measure_option, |
| callbacks=[ |
| autotvm.callback.progress_bar(tsk_trial, prefix=prefix), |
| autotvm.callback.log_to_file(tmp_log_file), |
| ], |
| ) |
| # Auto Tuning Stage 4: Pick the best performing configurations from the overall log. |
| autotvm.record.pick_best(tmp_log_file, tune_log) |
| |
| |
| |
| |
| |
| |
| |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 378-382 |
| |
| Enable OpenCLML Offloading |
| -------------------------- |
| OpenCLML offloading will try to accelerate supported operators |
| by using OpenCLML proprietory operator library. |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 382-388 |
| |
| .. code-block:: default |
| |
| |
| # By default :code:`enable_clml` is set to False in above configuration section. |
| |
| if not local_demo and enable_clml: |
| mod = clml.partition_for_clml(mod, params) |
| |
| |
| |
| |
| |
| |
| |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 389-392 |
| |
| Compilation |
| ----------- |
| Use tuning cache if exists. |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 392-400 |
| |
| .. code-block:: default |
| |
| if os.path.exists(tune_log): |
| with autotvm.apply_history_best(tune_log): |
| with tvm.transform.PassContext(opt_level=3): |
| lib = relay.build(mod, target=target, params=params) |
| else: |
| with tvm.transform.PassContext(opt_level=3): |
| lib = relay.build(mod, target=target, params=params) |
| |
| |
| |
| |
| |
| |
| |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 401-405 |
| |
| Deploy the Model Remotely by RPC |
| -------------------------------- |
| Using RPC you can deploy the model from host |
| machine to the remote Adreno device |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 405-429 |
| |
| .. code-block:: default |
| |
| if local_demo: |
| remote = rpc.LocalSession() |
| else: |
| tracker = rpc.connect_tracker(rpc_tracker_host, rpc_tracker_port) |
| # When running a heavy model, we should increase the `session_timeout` |
| remote = tracker.request(key, priority=0, session_timeout=60) |
| |
| if local_demo: |
| dev = remote.cpu(0) |
| elif test_target.find("opencl"): |
| dev = remote.cl(0) |
| else: |
| dev = remote.cpu(0) |
| |
| temp = utils.tempdir() |
| dso_binary = "dev_lib_cl.so" |
| dso_binary_path = temp.relpath(dso_binary) |
| fcompile = ndk.create_shared if not local_demo else None |
| lib.export_library(dso_binary_path, fcompile=fcompile) |
| remote_path = "/data/local/tmp/" + dso_binary |
| remote.upload(dso_binary_path) |
| rlib = remote.load_module(dso_binary) |
| m = graph_executor.GraphModule(rlib["default"](dev)) |
| |
| |
| |
| |
| |
| |
| |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 430-433 |
| |
| Run inference |
| ------------- |
| We now can set inputs, infer our model and get predictions as output |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 433-437 |
| |
| .. code-block:: default |
| |
| m.set_input(input_name, tvm.nd.array(img.astype("float32"))) |
| m.run() |
| tvm_output = m.get_output(0) |
| |
| |
| |
| |
| |
| |
| |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 438-442 |
| |
| Get predictions and performance statistic |
| ----------------------------------------- |
| This piece of code displays the top-1 and top-5 predictions, as |
| well as provides information about the model's performance |
| |
| .. GENERATED FROM PYTHON SOURCE LINES 442-474 |
| |
| .. code-block:: default |
| |
| from os.path import join, isfile |
| from matplotlib import pyplot as plt |
| from tvm.contrib import download |
| |
| # Download ImageNet categories |
| categ_url = "https://github.com/uwsampl/web-data/raw/main/vta/models/" |
| categ_fn = "synset.txt" |
| download.download(join(categ_url, categ_fn), categ_fn) |
| synset = eval(open(categ_fn).read()) |
| |
| top_categories = np.argsort(tvm_output.asnumpy()[0]) |
| top5 = np.flip(top_categories, axis=0)[:5] |
| |
| # Report top-1 classification result |
| print("Top-1 id: {}, class name: {}".format(top5[1 - 1], synset[top5[1 - 1]])) |
| |
| # Report top-5 classification results |
| print("\nTop5 predictions: \n") |
| print("\t#1:", synset[top5[1 - 1]]) |
| print("\t#2:", synset[top5[2 - 1]]) |
| print("\t#3:", synset[top5[3 - 1]]) |
| print("\t#4:", synset[top5[4 - 1]]) |
| print("\t#5:", synset[top5[5 - 1]]) |
| print("\t", top5) |
| ImageNetClassifier = False |
| for k in top_categories[-5:]: |
| if "cat" in synset[k]: |
| ImageNetClassifier = True |
| assert ImageNetClassifier, "Failed ImageNet classifier validation check" |
| |
| print("Evaluate inference time cost...") |
| print(m.benchmark(dev, number=1, repeat=10)) |
| |
| |
| |
| |
| .. rst-class:: sphx-glr-script-out |
| |
| .. code-block:: none |
| |
| /workspace/python/tvm/runtime/ndarray.py:217: DeprecationWarning: NDArray.asnumpy() will be deprecated in TVM v0.8 release. Please use NDArray.numpy() instead. |
| warnings.warn( |
| Top-1 id: 281, class name: tabby, tabby cat |
| |
| Top5 predictions: |
| |
| #1: tabby, tabby cat |
| #2: tiger cat |
| #3: lynx, catamount |
| #4: red fox, Vulpes vulpes |
| #5: Egyptian cat |
| [281 282 287 277 285] |
| Evaluate inference time cost... |
| Execution time summary: |
| mean (ms) median (ms) max (ms) min (ms) std (ms) |
| 4127.8531 4127.8278 4130.5083 4124.9284 1.6534 |
| |
| |
| |
| |
| |
| .. rst-class:: sphx-glr-timing |
| |
| **Total running time of the script:** ( 1 minutes 22.850 seconds) |
| |
| |
| .. _sphx_glr_download_how_to_deploy_models_deploy_model_on_adreno.py: |
| |
| .. only:: html |
| |
| .. container:: sphx-glr-footer sphx-glr-footer-example |
| |
| |
| .. container:: sphx-glr-download sphx-glr-download-python |
| |
| :download:`Download Python source code: deploy_model_on_adreno.py <deploy_model_on_adreno.py>` |
| |
| .. container:: sphx-glr-download sphx-glr-download-jupyter |
| |
| :download:`Download Jupyter notebook: deploy_model_on_adreno.ipynb <deploy_model_on_adreno.ipynb>` |
| |
| |
| .. only:: html |
| |
| .. rst-class:: sphx-glr-signature |
| |
| `Gallery generated by Sphinx-Gallery <https://sphinx-gallery.github.io>`_ |