code: def start_spot_workers(
      self,
      queue_name,
      bid_price,
      resources_needed={},
      ssh_keypair=None,
      queue_upscaling=True,
      start_workers=1,
      max_workers=100,
      timeout=300,
      ports=[],
      autoscaling_group_name=None,
  ):
      # TODO should be able to put bid price as None,
      # which means price of on-demand instance
      # or maybe specify bid_price as a fraction of on-demand price
  
      instance_type = self._select_instance_type(resources_needed)
  
      asg_name = (
          "studioml_autoscaling_" + queue_name or autoscaling_group_name
      )
      launch_config_name = asg_name + "_launch_config"
  
      startup_script = self._get_startup_script(
          resources_needed, queue_name, asg_name, timeout=timeout
      )
  
      if bid_price.endswith("%"):
          bid_price = str(
              self.prices[instance_type]
              * float(bid_price.replace("%", ""))
              / 100
          )
  
      self.logger.info(
          "Price bid for instance type {} : {}".format(
              instance_type, bid_price
          )
      )
  
      launch_config = {
          "ImageId": self._get_image_id(),
          "UserData": startup_script,
          "InstanceType": instance_type,
          "BlockDeviceMappings": self._get_block_device_mappings(
              resources_needed
          ),
          "InstanceMonitoring": {"Enabled": False},
          "SpotPrice": bid_price,
      }
  
      launch_config_name = (
          "studioml_launch_config_"
          + hashlib.sha256(
              json.dumps(launch_config, sort_keys=True).encode("utf-8")
          ).hexdigest()
      )
  
      if ssh_keypair is not None:
          ports.append(22)
      if any(ports):
          groupid = self._get_security_group(ports)
          launch_config["SecurityGroups"] = [groupid]
          if ssh_keypair is not None:
              launch_config["KeyName"] = ssh_keypair
  
      try:
          response = self.asclient.create_launch_configuration(
              LaunchConfigurationName=launch_config_name,
              **launch_config,
          )
          self.logger.debug(
              "create_launch_configuration response:\n {}".format(
                  response
              )
          )
      except self.asclient.exceptions.AlreadyExistsFault:
          self.logger.debug(
              "Launch config {} already exists".format(
                  launch_config_name
              )
          )
  
      asg_config = {
          "LaunchConfigurationName": launch_config_name,
          "MinSize": 0,
          "MaxSize": max_workers,
          "DesiredCapacity": int(start_workers),
          "LoadBalancerNames": [],
          "AvailabilityZones": [self.region + "a"],
          "TerminationPolicies": ["NewestInstance"],
          "DefaultCooldown": 0,
          "NewInstancesProtectedFromScaleIn": True,
      }
  
      self.logger.debug("Creating auto-scaling group " + asg_name)
  
      try:
          response = self.asclient.create_auto_scaling_group(
              AutoScalingGroupName=asg_name, **asg_config
          )
      except self.asclient.exceptions.AlreadyExistsFault:
          self.logger.debug(
              "Autoscaling group {} already exists".format(asg_name)
          )
  
      if queue_upscaling:
          max_steps = 20
          stepsize = -(-max_workers // max_steps)
  
          step_adjustments = [
              {
                  "MetricIntervalLowerBound": i,
                  "MetricIntervalUpperBound": i + stepsize,
                  "ScalingAdjustment": i,
              }
              for i in range(0, max_workers, stepsize)
          ]
  
          del step_adjustments[-1]["MetricIntervalUpperBound"]
  
          scaleup_policy_response = self.asclient.put_scaling_policy(
              AutoScalingGroupName=asg_name,
              PolicyName=asg_name + "_scaleup",
              PolicyType="StepScaling",
              AdjustmentType="ChangeInCapacity",
              StepAdjustments=step_adjustments,
          )
  
          self.cwclient.put_metric_alarm(
              AlarmName=asg_name + "_scaleup_alarm",
              MetricName="ApproximateNumberOfMessagesVisible",
              Namespace="AWS/SQS",
              Statistic="Average",
              Period=300,
              Threshold=1,
              ComparisonOperator="GreaterThanOrEqualToThreshold",
              EvaluationPeriods=1,
              AlarmActions=[scaleup_policy_response["PolicyARN"]],
              Dimensions=[{"Name": "QueueName", "Value": queue_name}],
          )
  
imports: import uuid
  import os
  import base64
  import json
  import yaml
  import six
  import filelock
  import hashlib
  import pickle
  from studio import git_util
  from studio.util import logs
  from studio.storage import storage_util
  from studio.util.gpu_util import memstr2int
  from .cloud_worker_util import insert_user_startup_script
label_desc: Use image node_id 9 for calling the EC2WorkerManager obj's underlying member method code with example usage: obj.start_spot_workers(queue_name, bid_price, resources_needed, ssh_keypair, queue_upscaling, start_workers, max_workers, timeout, ports, autoscaling_group_name) without return types