diff --git a/TensorStack.Python/Config/EnvironmentConfig.cs b/TensorStack.Python/Config/EnvironmentConfig.cs index 7545cae..d9731fc 100644 --- a/TensorStack.Python/Config/EnvironmentConfig.cs +++ b/TensorStack.Python/Config/EnvironmentConfig.cs @@ -17,9 +17,9 @@ public record EnvironmentConfig [ "typing==3.7.4.3", "wheel==0.46.3", - "transformers==4.57.6", + "transformers==5.5.4", "accelerate==1.13.0", - "diffusers@https://github.com/huggingface/diffusers/archive/d0c9cbad28d7d3bba28db94622e13500c4179075.zip", + "diffusers@https://github.com/huggingface/diffusers/archive/f3d42be118f9af7ed9697b686fba09a8bdcd71d1.zip", "protobuf==7.34.1", "sentencepiece==0.2.1", "ftfy==6.3.1", @@ -30,7 +30,8 @@ public record EnvironmentConfig "gguf==0.18.0", "av==17.0.1", "optimum-quanto==0.2.7", - "bitsandbytes==0.49.2" + "bitsandbytes==0.49.2", + "soundfile==0.13.1" ]; diff --git a/TensorStack.Python/Pipelines/IdeogramPipeline.py b/TensorStack.Python/Pipelines/IdeogramPipeline.py new file mode 100644 index 0000000..3ad2aa2 --- /dev/null +++ b/TensorStack.Python/Pipelines/IdeogramPipeline.py @@ -0,0 +1,467 @@ +import tensorstack.utils as Utils +import tensorstack.data_objects as DataObjects +import tensorstack.quantization as Quantization +from tensorstack.enums import ProcessType, QuantTarget +Utils.redirect_output() +Utils.create_services() + +import torch +import numpy as np +from pathlib import Path +from threading import Event +from collections.abc import Buffer +from typing import Dict, Sequence, List, Tuple, Optional, Any +from transformers import Qwen2Tokenizer, Qwen3VLModel +from diffusers import ( + AutoencoderKLFlux2, + Ideogram4Transformer2DModel, + Ideogram4Pipeline +) + +# Globals +_config = None +_model_config = None +_pipeline = None +_processType = None +_execution_device = None +_device_map = None +_pipeline_device_map = None +_control_net_name = None +_control_net_cache = None +_generator = None +_isMemoryOffload = False +_prompt_cache_key = None +_prompt_cache_value = None +_cancel_event = Event() +_stopwatch = None +_pipelineMap = { + ProcessType.TextToImage: Ideogram4Pipeline, +} + + +#------------------------------------------------ +# Load Pipeline +#------------------------------------------------ +def load(config_args: Dict[str, Any]) -> bool: + global _config, _pipeline, _generator, _processType, _execution_device, _isMemoryOffload + + # Config + _config = DataObjects.PipelineConfig(**config_args) + _execution_device = Utils.get_execution_device(_config) + _generator = torch.Generator(device=_execution_device) + _processType = _config.process_type + + # Initialize Pipeline + _pipeline = initialize(_config) + + # Load Lora + Utils.load_lora_weights(_pipeline, _config) + + # Memory + _isMemoryOffload = Utils.configure_pipeline_memory(_pipeline, _execution_device, _config) + Utils.trim_memory(_isMemoryOffload) + return True + + +#------------------------------------------------ +# Reload Pipeline - ProcessType, LoraAdapters and ControlNet are the only options that can be modified +#------------------------------------------------ +def reload(config_args: Dict[str, Any]) -> bool: + global _config, _pipeline, _processType + + # Config + _config = DataObjects.PipelineConfig(**config_args) + _processType = _config.process_type + + # Rebuild Pipeline + _pipeline.unload_lora_weights() + _pipeline = create_pipeline(_config) + + # Load Lora + Utils.load_lora_weights(_pipeline, _config) + + # Memory + Utils.configure_pipeline_memory(_pipeline, _execution_device, _config) + Utils.trim_memory(_isMemoryOffload) + return True + + +#------------------------------------------------ +# Switch Pipeline - ProcessType +#------------------------------------------------ +def switch(process_type: ProcessType) -> bool: + global _pipeline, _processType + + # Switch Pipeline + current = _processType + _processType = process_type + _pipeline = create_pipeline(_config) + + print(f"[Generate] Switched pipeline: {current} => {process_type}") + return True + + +#------------------------------------------------ +# Cancel Generation +#------------------------------------------------ +def generateCancel() -> None: + _cancel_event.set() + + +#------------------------------------------------ +# Unload Pipline +#------------------------------------------------ +def unload() -> bool: + global _pipeline, _prompt_cache_key, _prompt_cache_value + _pipeline = None + _prompt_cache_key = None + _prompt_cache_value = None + Utils.trim_memory(_isMemoryOffload) + return True + + +#------------------------------------------------ +# Get the notifications +#------------------------------------------------ +def getNotifications() -> list[(str, Buffer)]: + return Utils.notification_get() + + +#------------------------------------------------ +# Get the log entires +#------------------------------------------------ +def getLogs() -> list[str]: + return Utils.get_output() + + +#------------------------------------------------ +# Diffusers pipeline callback to capture step artifacts +#------------------------------------------------ +def _progress_callback(pipe, step: int, total_steps: int, info: Dict): + if _cancel_event.is_set(): + pipe._interrupt = True + raise Exception("Operation Canceled") + + steps = pipe._num_timesteps + elapsed = _stopwatch.reset() + step_latents = info.get("latents") + step_latents = step_latents.float().cpu() if step_latents is not None else [] + Utils.notification_push(key="Generate", subkey="Step", value=step + 1, maximum=steps, elapsed=elapsed, tensor=step_latents) + return info + + +#------------------------------------------------ +# Initialize Pipeline +#------------------------------------------------ +def initialize(config: DataObjects.PipelineConfig): + global _model_config, _device_map, _pipeline_device_map + + _device_map = Utils.get_device_map(config, _execution_device) + _pipeline_device_map = Utils.get_pipeline_device_map(config, _execution_device) + _model_config = Utils.get_model_config(__file__, config) + return create_pipeline(config) + + +#------------------------------------------------ +# Load Qwen2Tokenizer +#------------------------------------------------ +def load_tokenizer(config: DataObjects.PipelineConfig, pipeline_kwargs: Dict[str, str]): + if _pipeline and _pipeline.tokenizer: + print(f"[Load] Loading Cached Tokenizer") + return _pipeline.tokenizer + + tokenizer_path: Path = _model_config["tokenizer"] + tokenizer_config: Path = _model_config["tokenizer_config"] + + # 1. Load from pretrained folder + print(f"[Load] Loading Pretrained Tokenizer") + tokenizer = Qwen2Tokenizer.from_pretrained( + tokenizer_path, + config=tokenizer_config, + dtype=config.data_type, + **pipeline_kwargs + ) + return tokenizer + + +#------------------------------------------------ +# Load Qwen3VLModel +#------------------------------------------------ +def load_text_encoder(config: DataObjects.PipelineConfig, pipeline_kwargs: Dict[str, str]): + if _pipeline and _pipeline.text_encoder: + print(f"[Load] Loading Cached TextEncoder") + return _pipeline.text_encoder + + text_encoder_path: Path = _model_config["text_encoder"] + text_encoder_config: Path = _model_config["text_encoder_config"] + + # 1. Load from pretrained folder + print(f"[Load] Loading Pretrained TextEncoder") + text_encoder = Qwen3VLModel.from_pretrained( + text_encoder_path, + config=text_encoder_config, + dtype=config.data_type, + device_map=_device_map, + quantization_config=Quantization.auto_pretrained_config(config, QuantTarget.TEXT_ENCODER), + **pipeline_kwargs + ) + Utils.trim_memory(True) + return text_encoder + + +#------------------------------------------------ +# Load Ideogram4Transformer2DModel +#------------------------------------------------ +def load_transformer(config: DataObjects.PipelineConfig, pipeline_kwargs: Dict[str, str]): + if _pipeline and _pipeline.transformer: + print(f"[Load] Loading Cached Transformer") + return _pipeline.transformer + + transformer_path: Path = _model_config["transformer"] + transformer_config: Path = _model_config["transformer_config"] + + # 1. Load from single file + if transformer_path.is_file(): + is_gguf = Utils.isGGUF(transformer_path) + print(f"[Load] Loading File Transformer") + transformer = Ideogram4Transformer2DModel.from_single_file( + str(transformer_path), + config=str(transformer_config), + torch_dtype=config.data_type, + device_map=_device_map, + quantization_config=Quantization.auto_single_file_config(config, QuantTarget.TRANSFORMER, is_gguf), + **pipeline_kwargs + ) + Quantization.quantize_model(config, transformer, is_gguf) + Utils.trim_memory(True) + return transformer + + # 2. Load from pretrained folder + print(f"[Load] Loading Pretrained Transformer") + transformer = Ideogram4Transformer2DModel.from_pretrained( + str(transformer_path), + torch_dtype=config.data_type, + device_map=_device_map, + quantization_config=Quantization.auto_pretrained_config(config, QuantTarget.TRANSFORMER), + **pipeline_kwargs + ) + Utils.trim_memory(True) + return transformer + + +#------------------------------------------------ +# Load Ideogram4Transformer2DModel +#------------------------------------------------ +def load_unconditional_transformer(config: DataObjects.PipelineConfig, pipeline_kwargs: Dict[str, str]): + if _pipeline and _pipeline.unconditional_transformer: + print(f"[Load] Loading Cached Unconditional Transformer") + return _pipeline.unconditional_transformer + + transformer_path: Path = _model_config["transformer_2"] + transformer_config: Path = _model_config["transformer_2_config"] + + # 1. Load from single file + if transformer_path.is_file(): + is_gguf = Utils.isGGUF(transformer_path) + print(f"[Load] Loading File Unconditional Transformer") + transformer = Ideogram4Transformer2DModel.from_single_file( + str(transformer_path), + config=str(transformer_config), + torch_dtype=config.data_type, + device_map=_device_map, + quantization_config=Quantization.auto_single_file_config(config, QuantTarget.TRANSFORMER, is_gguf), + **pipeline_kwargs + ) + Quantization.quantize_model(config, transformer, is_gguf) + Utils.trim_memory(True) + return transformer + + # 2. Load from pretrained folder + print(f"[Load] Loading Pretrained Unconditional Transformer") + transformer = Ideogram4Transformer2DModel.from_pretrained( + str(transformer_path), + torch_dtype=config.data_type, + device_map=_device_map, + quantization_config=Quantization.auto_pretrained_config(config, QuantTarget.TRANSFORMER), + **pipeline_kwargs + ) + Utils.trim_memory(True) + return transformer + + +#------------------------------------------------ +# Load AutoencoderKLFlux2 +#------------------------------------------------ +def load_vae(config: DataObjects.PipelineConfig, pipeline_kwargs: Dict[str, str]): + if _pipeline and _pipeline.vae: + print(f"[Load] Loading Cached Vae") + return _pipeline.vae + + vae_path: Path = _model_config["vae"] + vae_config: Path = _model_config["vae_config"] + single_path: Path = _model_config["single_file"] + template_path: Path = _model_config["template"] + + # 1. Load from single file + if vae_path.is_file(): + print(f"[Load] Loading SingleFile Vae") + auto_encoder = AutoencoderKLFlux2.from_single_file( + str(vae_path), + config=str(vae_config), + torch_dtype=config.data_type, + device_map=_device_map, + **pipeline_kwargs + ) + Utils.trim_memory(True) + return auto_encoder + + # 2. Load component from single file + if single_path and single_path.is_file(): + print(f"[Load] Loading Component Vae") + auto_encoder = Utils.from_component(Ideogram4Pipeline, "vae", single_path, template_path, _device_map, config.data_type) + if auto_encoder: + Utils.trim_memory(True) + return auto_encoder + + # 3. Load from pretrained folder + print(f"[Load] Loading Pretrained Vae") + auto_encoder = AutoencoderKLFlux2.from_pretrained( + str(vae_path), + torch_dtype=config.data_type, + device_map=_device_map, + **pipeline_kwargs + ) + Utils.trim_memory(True) + return auto_encoder + + +#------------------------------------------------ +# Load ControlNetModel +#------------------------------------------------ +def load_control_net(config: DataObjects.PipelineConfig, pipeline_kwargs: Dict[str, str]): + global _control_net_name, _control_net_cache + + if _control_net_cache and _control_net_name == config.control_net.name: + print(f"[Load] Loading Cached ControlNet") + return _control_net_cache + + if config.control_net.name is None: + _control_net_name = None + _control_net_cache = None + return None + + # print(f"[Load] Loading Pretrained ControlNet") + # _control_net_name = config.control_net.name + # _control_net_cache = ControlNetModel.from_pretrained( + # config.control_net.path, + # torch_dtype=config.data_type, + # device_map=_device_map, + # **pipeline_kwargs + # ) + return _control_net_cache + + +#------------------------------------------------ +# Create a new pipeline +#------------------------------------------------ +def create_pipeline(config: DataObjects.PipelineConfig): + template_path: Path = _model_config["template"] + pipeline_kwargs = { + "variant": config.variant, + "use_safetensors":True, + "low_cpu_mem_usage":True, + "local_files_only":True, + } + + # Load Models + tokenizer = load_tokenizer(config, pipeline_kwargs) + text_encoder = load_text_encoder(config, pipeline_kwargs) + transformer = load_transformer(config, pipeline_kwargs) + unconditional_transformer = load_unconditional_transformer(config, pipeline_kwargs) + vae = load_vae(config, pipeline_kwargs) + control_net = load_control_net(config, pipeline_kwargs) + if control_net is not None: + pipeline_kwargs.update({"controlnet": control_net}) + + # Build Pipeline + pipeline = _pipelineMap[_processType] + return pipeline.from_pretrained( + template_path, + tokenizer=tokenizer, + text_encoder=text_encoder, + transformer=transformer, + unconditional_transformer=unconditional_transformer, + vae=vae, + torch_dtype=config.data_type, + device_map=_pipeline_device_map, + **pipeline_kwargs + ) + + +#------------------------------------------------ +# Generate Image/Video +#------------------------------------------------ +def generate( + inference_args: Dict[str, Any], + input_tensors: Optional[List[Tuple[Sequence[float],Sequence[int]]]] = None, + control_tensors: Optional[List[Tuple[Sequence[float],Sequence[int]]]] = None, + ) -> Sequence[Buffer]: + global _prompt_cache_key, _prompt_cache_value, _stopwatch + _cancel_event.clear() + _pipeline._interrupt = False + _stopwatch = Utils.Stopwatch() + _stopwatch.start() + + # Input Images + images = Utils.prepare_images(input_tensors) + image_count = Utils.get_len(images) + control_images = Utils.prepare_images(control_tensors) + control_image_count = Utils.get_len(control_images) + print(f"[Generate] Input Received - Tensors: {image_count}, Control Tensors: {control_image_count}") + + # Options + options = DataObjects.PipelineOptions(**inference_args) + + # Scheduler + _pipeline.scheduler = Utils.create_scheduler(options.scheduler_options) + + # AutoEncoder + Utils.configure_vae_memory(_pipeline, options.enable_vae_tiling, options.enable_vae_slicing) + + # Lora Adapters + Utils.set_lora_weights(_pipeline, options) + + # Notify + Utils.notification_push(key="Generate", subkey="Initialize", elapsed=_stopwatch.reset()) + + # Prompt Cache + + # Notify + Utils.notification_push(key="Generate", subkey="Encode", elapsed=_stopwatch.reset()) + + # Pipeline Options + pipeline_options = { + "prompt": options.prompt, + "height": options.height, + "width": options.width, + "generator": _generator.manual_seed(options.seed), + #"guidance_scale": options.guidance_scale, + "num_inference_steps": options.steps, + "output_type": "np", + "callback_on_step_end": _progress_callback, + "callback_on_step_end_tensor_inputs": ["latents"], + } + + # Run Pipeline + output = _pipeline(**pipeline_options)[0] + + # (Batch, Channel, Height, Width) + output = output.transpose(0, 3, 1, 2).astype(np.float32) + + # Notify + Utils.notification_push(key="Generate", subkey="Decode", elapsed = _stopwatch.reset()) + Utils.notification_push(key="Generate", subkey="Complete", elapsed = _stopwatch.stop()) + + # Cleanup + Utils.trim_memory(_isMemoryOffload) + return [ np.ascontiguousarray(output) ] \ No newline at end of file diff --git a/TensorStack.Python/Pipelines/Templates/Ideogram4/model_index.json b/TensorStack.Python/Pipelines/Templates/Ideogram4/model_index.json new file mode 100644 index 0000000..93aa0b4 --- /dev/null +++ b/TensorStack.Python/Pipelines/Templates/Ideogram4/model_index.json @@ -0,0 +1,28 @@ +{ + "_class_name": "Ideogram4Pipeline", + "_diffusers_version": "0.39.0.dev0", + "scheduler": [ + "diffusers", + "FlowMatchEulerDiscreteScheduler" + ], + "text_encoder": [ + "transformers", + "Qwen3VLModel" + ], + "tokenizer": [ + "transformers", + "Qwen2Tokenizer" + ], + "transformer": [ + "diffusers", + "Ideogram4Transformer2DModel" + ], + "unconditional_transformer": [ + "diffusers", + "Ideogram4Transformer2DModel" + ], + "vae": [ + "diffusers", + "AutoencoderKLFlux2" + ] +} diff --git a/TensorStack.Python/Pipelines/Templates/Ideogram4/scheduler/scheduler_config.json b/TensorStack.Python/Pipelines/Templates/Ideogram4/scheduler/scheduler_config.json new file mode 100644 index 0000000..33c1410 --- /dev/null +++ b/TensorStack.Python/Pipelines/Templates/Ideogram4/scheduler/scheduler_config.json @@ -0,0 +1,18 @@ +{ + "_class_name": "FlowMatchEulerDiscreteScheduler", + "_diffusers_version": "0.39.0.dev0", + "base_image_seq_len": 256, + "base_shift": 0.5, + "invert_sigmas": false, + "max_image_seq_len": 4096, + "max_shift": 1.15, + "num_train_timesteps": 1000, + "shift": 1.0, + "shift_terminal": null, + "stochastic_sampling": false, + "time_shift_type": "exponential", + "use_beta_sigmas": false, + "use_dynamic_shifting": false, + "use_exponential_sigmas": false, + "use_karras_sigmas": false +} diff --git a/TensorStack.Python/Pipelines/Templates/Ideogram4/text_encoder/config.json b/TensorStack.Python/Pipelines/Templates/Ideogram4/text_encoder/config.json new file mode 100644 index 0000000..180cbe5 --- /dev/null +++ b/TensorStack.Python/Pipelines/Templates/Ideogram4/text_encoder/config.json @@ -0,0 +1,65 @@ +{ + "architectures": [ + "Qwen3VLModel" + ], + "dtype": "bfloat16", + "image_token_id": 151655, + "model_type": "qwen3_vl", + "text_config": { + "attention_bias": false, + "attention_dropout": 0.0, + "bos_token_id": 151643, + "dtype": "bfloat16", + "eos_token_id": 151645, + "head_dim": 128, + "hidden_act": "silu", + "hidden_size": 4096, + "initializer_range": 0.02, + "intermediate_size": 12288, + "max_position_embeddings": 262144, + "model_type": "qwen3_vl_text", + "num_attention_heads": 32, + "num_hidden_layers": 36, + "num_key_value_heads": 8, + "pad_token_id": null, + "rms_norm_eps": 1e-06, + "rope_parameters": { + "mrope_interleaved": true, + "mrope_section": [ + 24, + 20, + 20 + ], + "rope_theta": 5000000, + "rope_type": "default" + }, + "use_cache": true, + "vocab_size": 151936 + }, + "tie_word_embeddings": false, + "transformers_version": "5.8.0", + "video_token_id": 151656, + "vision_config": { + "deepstack_visual_indexes": [ + 8, + 16, + 24 + ], + "depth": 27, + "dtype": "bfloat16", + "hidden_act": "gelu_pytorch_tanh", + "hidden_size": 1152, + "in_channels": 3, + "initializer_range": 0.02, + "intermediate_size": 4304, + "model_type": "qwen3_vl_vision", + "num_heads": 16, + "num_position_embeddings": 2304, + "out_hidden_size": 4096, + "patch_size": 16, + "spatial_merge_size": 2, + "temporal_patch_size": 2 + }, + "vision_end_token_id": 151653, + "vision_start_token_id": 151652 +} \ No newline at end of file diff --git a/TensorStack.Python/Pipelines/Templates/Ideogram4/tokenizer/tokenizer_config.json b/TensorStack.Python/Pipelines/Templates/Ideogram4/tokenizer/tokenizer_config.json new file mode 100644 index 0000000..2531cbf --- /dev/null +++ b/TensorStack.Python/Pipelines/Templates/Ideogram4/tokenizer/tokenizer_config.json @@ -0,0 +1,30 @@ +{ + "add_prefix_space": false, + "backend": "tokenizers", + "bos_token": null, + "clean_up_tokenization_spaces": false, + "eos_token": "<|im_end|>", + "errors": "replace", + "extra_special_tokens": [ + "<|im_start|>", + "<|im_end|>", + "<|object_ref_start|>", + "<|object_ref_end|>", + "<|box_start|>", + "<|box_end|>", + "<|quad_start|>", + "<|quad_end|>", + "<|vision_start|>", + "<|vision_end|>", + "<|vision_pad|>", + "<|image_pad|>", + "<|video_pad|>" + ], + "is_local": true, + "local_files_only": false, + "model_max_length": 262144, + "pad_token": "<|endoftext|>", + "split_special_tokens": false, + "tokenizer_class": "Qwen2Tokenizer", + "unk_token": null +} diff --git a/TensorStack.Python/Pipelines/Templates/Ideogram4/transformer/config.json b/TensorStack.Python/Pipelines/Templates/Ideogram4/transformer/config.json new file mode 100644 index 0000000..ef3ed30 --- /dev/null +++ b/TensorStack.Python/Pipelines/Templates/Ideogram4/transformer/config.json @@ -0,0 +1,18 @@ +{ + "_class_name": "Ideogram4Transformer2DModel", + "_diffusers_version": "0.39.0.dev0", + "adaln_dim": 512, + "attention_head_dim": 256, + "in_channels": 128, + "intermediate_size": 12288, + "llm_features_dim": 53248, + "mrope_section": [ + 24, + 20, + 20 + ], + "norm_eps": 0.00001, + "num_attention_heads": 18, + "num_layers": 34, + "rope_theta": 5000000 +} diff --git a/TensorStack.Python/Pipelines/Templates/Ideogram4/unconditional_transformer/config.json b/TensorStack.Python/Pipelines/Templates/Ideogram4/unconditional_transformer/config.json new file mode 100644 index 0000000..ef3ed30 --- /dev/null +++ b/TensorStack.Python/Pipelines/Templates/Ideogram4/unconditional_transformer/config.json @@ -0,0 +1,18 @@ +{ + "_class_name": "Ideogram4Transformer2DModel", + "_diffusers_version": "0.39.0.dev0", + "adaln_dim": 512, + "attention_head_dim": 256, + "in_channels": 128, + "intermediate_size": 12288, + "llm_features_dim": 53248, + "mrope_section": [ + 24, + 20, + 20 + ], + "norm_eps": 0.00001, + "num_attention_heads": 18, + "num_layers": 34, + "rope_theta": 5000000 +} diff --git a/TensorStack.Python/Pipelines/Templates/Ideogram4/vae/config.json b/TensorStack.Python/Pipelines/Templates/Ideogram4/vae/config.json new file mode 100644 index 0000000..9d324c9 --- /dev/null +++ b/TensorStack.Python/Pipelines/Templates/Ideogram4/vae/config.json @@ -0,0 +1,40 @@ +{ + "_class_name": "AutoencoderKLFlux2", + "_diffusers_version": "0.39.0.dev0", + "act_fn": "silu", + "batch_norm_eps": 0.0001, + "batch_norm_momentum": 0.1, + "block_out_channels": [ + 128, + 256, + 512, + 512 + ], + "decoder_block_out_channels": null, + "down_block_types": [ + "DownEncoderBlock2D", + "DownEncoderBlock2D", + "DownEncoderBlock2D", + "DownEncoderBlock2D" + ], + "force_upcast": true, + "in_channels": 3, + "latent_channels": 32, + "layers_per_block": 2, + "mid_block_add_attention": true, + "norm_num_groups": 32, + "out_channels": 3, + "patch_size": [ + 2, + 2 + ], + "sample_size": 1024, + "up_block_types": [ + "UpDecoderBlock2D", + "UpDecoderBlock2D", + "UpDecoderBlock2D", + "UpDecoderBlock2D" + ], + "use_post_quant_conv": true, + "use_quant_conv": true +} diff --git a/TensorStack.Python/TensorStack.Python.csproj b/TensorStack.Python/TensorStack.Python.csproj index 21fd455..cfc1f42 100644 --- a/TensorStack.Python/TensorStack.Python.csproj +++ b/TensorStack.Python/TensorStack.Python.csproj @@ -105,6 +105,12 @@ + + + + + +