diff --git a/typescript/batch-ecr-openmp/README.md b/typescript/batch-ecr-openmp/README.md new file mode 100644 index 000000000..11b51c1d4 --- /dev/null +++ b/typescript/batch-ecr-openmp/README.md @@ -0,0 +1,264 @@ +# AWS Batch with ECR and Lambda + +--- + +![Stability: Stable](https://img.shields.io/badge/stability-Stable-success.svg?style=for-the-badge) + +> **This is a stable example. It should successfully build out of the box** +> +> This examples is built on Construct Libraries marked "Stable" and does not have any infrastructure prerequisites to build. + +--- + + +This example demonstrates how to use AWS Batch with a containerized C++ OpenMP application stored in Amazon ECR, with optional Lambda function for job submission. The OpenMP application performs parallel computing benchmarks (arithmetic operations, mathematical functions, matrix multiplication) to demonstrate CPU-intensive workloads and compare sequential vs parallel execution performance. + +## Prerequisites + +Before deploying, ensure you have: +- AWS CLI configured with appropriate credentials +- Docker installed and running +- Node.js and npm installed +- `jq` command-line JSON processor (for job submission script) + +## Quick Start + +For a complete end-to-end deployment and testing: + +```bash +# 1. Test locally first (optional but recommended) +./scripts/test-local.sh + +# 2. Deploy everything to AWS +AWS_PROFILE=your-profile ./scripts/build-and-deploy.sh + +# 3. Submit a benchmark job +./scripts/submit-job.sh --benchmark-type simple +``` + +## Build + +To build this app manually, you need to be in this example's root folder. Then run the following: + +```bash +npm install -g aws-cdk +npm install +npm run build +``` + +This will install the necessary CDK, then this example's dependencies, and then build your TypeScript files and your CloudFormation template. + +## Deploy + +### Option 1: Automated Deployment (Recommended) + +Use the provided script for a complete deployment: + +```bash +AWS_PROFILE=your-profile ./scripts/build-and-deploy.sh +``` + +This script will: +1. Build the CDK stack +2. Deploy the infrastructure to AWS +3. Build and push the Docker image to ECR +4. Save deployment information for job submission + +### Option 2: Manual Deployment + +For manual control over each step: + +```bash +# Deploy CDK stack +npx cdk deploy --profile your-profile + +# Get ECR login and build/push Docker image +aws ecr get-login-password --region your-region | docker login --username AWS --password-stdin your-account.dkr.ecr.your-region.amazonaws.com +docker build -f docker/Dockerfile -t openmp-benchmark:latest . +docker tag openmp-benchmark:latest your-account.dkr.ecr.your-region.amazonaws.com/openmp-benchmark:latest +docker push your-account.dkr.ecr.your-region.amazonaws.com/openmp-benchmark:latest +``` + +After deployment, you will see the Stack outputs, which include the ECR repository URI and Batch job queue name. + +## Running Benchmarks + +After successful deployment, you can submit OpenMP benchmark jobs using the provided script: + +### Basic Usage + +```bash +# Submit a simple benchmark (default) +./scripts/submit-job.sh + +# Submit specific benchmark types +./scripts/submit-job.sh --benchmark-type math +./scripts/submit-job.sh --benchmark-type matrix +./scripts/submit-job.sh --benchmark-type heavy +./scripts/submit-job.sh --benchmark-type all +``` + +### Advanced Options + +```bash +# Custom parameters +./scripts/submit-job.sh --benchmark-type matrix --instance c6i.2xlarge --threads 8 + +# Submit via Lambda function +./scripts/submit-job.sh --method lambda --benchmark-type simple + +# Preview commands without executing +./scripts/submit-job.sh --dry-run --benchmark-type all +``` + +### Available Benchmark Types + +- **simple** - Basic arithmetic operations (~2 seconds) +- **math** - Mathematical functions: sin, cos, sqrt, pow (~5 seconds) +- **matrix** - Matrix multiplication (memory intensive, ~3 seconds) +- **heavy** - Complex arithmetic expressions (~3 seconds) +- **all** - Run all benchmarks sequentially (~15 seconds total) + +### Monitoring Jobs + +After submitting a job, you can monitor it through: + +1. **AWS Console**: + - Batch: https://console.aws.amazon.com/batch/ + - CloudWatch Logs: https://console.aws.amazon.com/cloudwatch/ + +2. **AWS CLI**: + ```bash + # Check job status + aws batch describe-jobs --profile your-profile --jobs + + # View logs + aws logs describe-log-streams --profile your-profile --log-group-name /aws/batch/openmp-benchmark + ``` + +## Testing + +### Local Testing + +Before deploying to AWS, you can test the OpenMP application locally: + +```bash +./scripts/test-local.sh +``` + +This script will: +1. Build the OpenMP application locally +2. Run C++ unit tests (8 test cases) +3. Execute integration tests with various parameters +4. Build and test the Docker container +5. Run CDK unit tests + +### What Gets Tested + +- ✓ OpenMP application builds successfully +- ✓ C++ unit tests pass (sequential vs parallel validation) +- ✓ Integration tests with different parameters +- ✓ Docker container builds and runs correctly +- ✓ CDK stack synthesizes without errors + +## The Component Structure + +The whole component contains: + +- An ECR repository for storing the Docker image +- AWS Batch compute environment with managed EC2 instances +- AWS Batch job definition and job queue +- Lambda function for job submission (optional) +- VPC with public and private subnets +- CloudWatch log group for job logs + +## CDK Toolkit + +The [`cdk.json`](./cdk.json) file in the root of this repository includes +instructions for the CDK toolkit on how to execute this program. + +After building your TypeScript code, you will be able to run the CDK toolkit commands as usual: + +```bash + $ cdk ls + + + $ cdk synth + + + $ cdk deploy + + + $ cdk diff + +``` + +## Cleanup + +To completely remove all AWS resources and avoid ongoing charges: + +### Step 1: Cancel Running Jobs + +Before destroying the stack, ensure no jobs are running: + +#### Option A: Using AWS Console (Visual Method) +1. Navigate to [AWS Batch Console](https://console.aws.amazon.com/batch/) +2. Click on "Jobs" in the left sidebar +3. Select your job queue (e.g., "AwsBatchOpenmpBenchmarkStack-OpenMPJobQueue...") +4. Filter by status: RUNNING, RUNNABLE, or PENDING +5. Select any active jobs and click "Terminate job" +6. Provide a reason (e.g., "Stack cleanup") +7. Verify all jobs show as SUCCEEDED, FAILED, or CANCELLED + +#### Option B: Using AWS CLI (Programmatic Method) +```bash +# List running jobs +aws batch list-jobs --profile your-profile \ + --job-queue \ + --job-status RUNNING + +# Terminate each job +aws batch terminate-job --profile your-profile \ + --job-id \ + --reason "Stack cleanup" +``` + +### Step 2: Destroy the Stack + +```bash +npx cdk destroy --profile your-profile +``` + +Type 'y' when prompted to confirm deletion. This will remove: +- ✓ All infrastructure resources (VPC, subnets, NAT gateway) +- ✓ ECR repository and all Docker images inside it +- ✓ Batch compute environment, job queue, and job definitions +- ✓ Lambda function and IAM roles +- ✓ CloudWatch log groups and all logs +- ✓ Security groups and all networking components + +### Step 3: Clean Up Local Artifacts (Optional) + +```bash +# Remove deployment info file +rm -f deployment-info.json + +# Remove all local Docker images (including tagged ECR images) +docker rmi $(docker images | grep openmp-benchmark | awk '{print $3}') + +# Clean up Docker build cache (frees significant disk space) +docker builder prune + +# Clean up any dangling Docker images +docker image prune +``` + +### Verification + +After cleanup, verify in the AWS Console that: +- The CloudFormation stack is deleted +- No ECR repositories remain for this project +- No VPC resources are left behind +- No unexpected charges appear in your AWS billing + +> **Note**: The stack is specifically configured for complete resource deletion. All resources have appropriate removal policies to ensure clean deletion without leaving orphaned resources. diff --git a/typescript/batch-ecr-openmp/bin/aws-batch-openmp-benchmark.ts b/typescript/batch-ecr-openmp/bin/aws-batch-openmp-benchmark.ts new file mode 100644 index 000000000..57ce45c38 --- /dev/null +++ b/typescript/batch-ecr-openmp/bin/aws-batch-openmp-benchmark.ts @@ -0,0 +1,20 @@ +#!/usr/bin/env node +import * as cdk from 'aws-cdk-lib'; +import { AwsBatchOpenmpBenchmarkStack } from '../lib/aws-batch-openmp-benchmark-stack'; + +const app = new cdk.App(); +new AwsBatchOpenmpBenchmarkStack(app, 'AwsBatchOpenmpBenchmarkStack', { + /* If you don't specify 'env', this stack will be environment-agnostic. + * Account/Region-dependent features and context lookups will not work, + * but a single synthesized template can be deployed anywhere. */ + + /* Uncomment the next line to specialize this stack for the AWS Account + * and Region that are implied by the current CLI configuration. */ + // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, + + /* Uncomment the next line if you know exactly what Account and Region you + * want to deploy the stack to. */ + // env: { account: '123456789012', region: 'us-east-1' }, + + /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ +}); diff --git a/typescript/batch-ecr-openmp/cdk.json b/typescript/batch-ecr-openmp/cdk.json new file mode 100644 index 000000000..b19706556 --- /dev/null +++ b/typescript/batch-ecr-openmp/cdk.json @@ -0,0 +1,97 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/aws-batch-openmp-benchmark.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, + "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, + "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, + "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, + "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, + "@aws-cdk/core:enableAdditionalMetadataCollection": true, + "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false, + "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true, + "@aws-cdk/aws-events:requireEventBusPolicySid": true, + "@aws-cdk/core:aspectPrioritiesMutating": true, + "@aws-cdk/aws-dynamodb:retainTableReplica": true, + "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true, + "@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": true, + "@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": true, + "@aws-cdk/aws-s3:publicAccessBlockedByDefault": true, + "@aws-cdk/aws-lambda:useCdkManagedLogGroup": true + } +} diff --git a/typescript/batch-ecr-openmp/docker/Dockerfile b/typescript/batch-ecr-openmp/docker/Dockerfile new file mode 100644 index 000000000..be221b985 --- /dev/null +++ b/typescript/batch-ecr-openmp/docker/Dockerfile @@ -0,0 +1,51 @@ +# Multi-stage build for OpenMP C++ application +FROM ubuntu:22.04 as builder + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + g++ \ + make \ + libomp-dev \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /build + +# Copy source code +COPY src/openmp/ . + +# Build the application +RUN make clean && make + +# Runtime stage +FROM ubuntu:22.04 + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + libgomp1 \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user for security +RUN useradd -m -u 1000 openmp + +# Copy the built executable +COPY --from=builder /build/openmp_benchmark /usr/local/bin/ + +# Set executable permissions +RUN chmod +x /usr/local/bin/openmp_benchmark + +# Switch to non-root user +USER openmp + +# Set working directory +WORKDIR /home/openmp + +# Default command +ENTRYPOINT ["/usr/local/bin/openmp_benchmark"] +CMD ["--json"] + +# Labels for metadata +LABEL maintainer="OpenMP Showcase" +LABEL description="C++20 OpenMP parallel computing benchmark for AWS Batch" +LABEL version="1.0" diff --git a/typescript/batch-ecr-openmp/jest.config.js b/typescript/batch-ecr-openmp/jest.config.js new file mode 100644 index 000000000..08263b895 --- /dev/null +++ b/typescript/batch-ecr-openmp/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + } +}; diff --git a/typescript/batch-ecr-openmp/lib/aws-batch-openmp-benchmark-stack.ts b/typescript/batch-ecr-openmp/lib/aws-batch-openmp-benchmark-stack.ts new file mode 100644 index 000000000..4bfa0b043 --- /dev/null +++ b/typescript/batch-ecr-openmp/lib/aws-batch-openmp-benchmark-stack.ts @@ -0,0 +1,320 @@ +import * as cdk from 'aws-cdk-lib'; +import * as batch from 'aws-cdk-lib/aws-batch'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as ecr from 'aws-cdk-lib/aws-ecr'; +import * as ecs from 'aws-cdk-lib/aws-ecs'; +import * as iam from 'aws-cdk-lib/aws-iam'; + +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as logs from 'aws-cdk-lib/aws-logs'; +import { Construct } from 'constructs'; + +export class AwsBatchOpenmpBenchmarkStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + /* + * COMPLETE DELETION CONFIGURATION + * ================================ + * This stack is configured for complete resource deletion when running 'cdk destroy': + * + * - ECR Repository: emptyOnDelete=true removes all images before deletion + + * - VPC: applyRemovalPolicy(DESTROY) ensures complete VPC cleanup + * - CloudWatch Logs: removalPolicy=DESTROY removes log groups + * - All other resources (IAM roles, security groups, Batch resources, Lambda) + * will be automatically deleted as they have no retention policies + * + * WARNING: Running 'cdk destroy' will permanently delete ALL data and resources! + */ + + + // ECR Repository for OpenMP container + const ecrRepository = new ecr.Repository(this, 'OpenMPRepository', { + repositoryName: 'openmp-benchmark', + removalPolicy: cdk.RemovalPolicy.DESTROY, + emptyOnDelete: true, // Force delete all images on stack deletion + lifecycleRules: [{ + maxImageCount: 10, + description: 'Keep only 10 most recent images' + }] + }); + + + + // VPC for Batch compute environment + const vpc = new ec2.Vpc(this, 'OpenMPVPC', { + maxAzs: 2, + natGateways: 1, + subnetConfiguration: [ + { + cidrMask: 24, + name: 'Public', + subnetType: ec2.SubnetType.PUBLIC, + }, + { + cidrMask: 24, + name: 'Private', + subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, + } + ] + }); + + // Apply removal policy to VPC to ensure complete deletion + vpc.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY); + + // Security Group for Batch compute environment + const batchSecurityGroup = new ec2.SecurityGroup(this, 'BatchSecurityGroup', { + vpc, + description: 'Security group for OpenMP Batch compute environment', + allowAllOutbound: true + }); + + // IAM Role for Batch service + const batchServiceRole = new iam.Role(this, 'BatchServiceRole', { + assumedBy: new iam.ServicePrincipal('batch.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSBatchServiceRole') + ] + }); + + // IAM Role for EC2 instances in Batch + const instanceRole = new iam.Role(this, 'BatchInstanceRole', { + assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonEC2ContainerServiceforEC2Role') + ] + }); + + + + // Instance Profile for EC2 instances + const instanceProfile = new iam.CfnInstanceProfile(this, 'BatchInstanceProfile', { + roles: [instanceRole.roleName] + }); + + // Launch Template for compute environment + const launchTemplate = new ec2.LaunchTemplate(this, 'BatchLaunchTemplate', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.LARGE), + machineImage: ec2.MachineImage.latestAmazonLinux2({ + edition: ec2.AmazonLinuxEdition.STANDARD + }), + securityGroup: batchSecurityGroup, + userData: ec2.UserData.forLinux(), + role: instanceRole + }); + + // Batch Compute Environment - using cost-effective instance types + const computeEnvironment = new batch.CfnComputeEnvironment(this, 'OpenMPComputeEnvironment', { + type: 'MANAGED', + state: 'ENABLED', + serviceRole: batchServiceRole.roleArn, + computeResources: { + type: 'EC2', + minvCpus: 0, + maxvCpus: 256, + instanceTypes: ['c6i.large', 'c6i.xlarge', 'c6i.2xlarge', 'c5.large', 'c5.xlarge'], + instanceRole: instanceProfile.attrArn, + securityGroupIds: [batchSecurityGroup.securityGroupId], + subnets: vpc.privateSubnets.map(subnet => subnet.subnetId) + } + }); + + // Batch Job Queue + const jobQueue = new batch.CfnJobQueue(this, 'OpenMPJobQueue', { + state: 'ENABLED', + priority: 1, + computeEnvironmentOrder: [ + { + computeEnvironment: computeEnvironment.ref, + order: 1 + } + ] + }); + + // IAM Role for Batch jobs + const jobRole = new iam.Role(this, 'BatchJobRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy') + ] + }); + + + + // CloudWatch Log Group for Batch jobs + const logGroup = new logs.LogGroup(this, 'OpenMPLogGroup', { + logGroupName: '/aws/batch/openmp-benchmark', + removalPolicy: cdk.RemovalPolicy.DESTROY, + retention: logs.RetentionDays.ONE_WEEK + }); + + // Batch Job Definition using CloudFormation with parameter mapping + const jobDefinition = new batch.CfnJobDefinition(this, 'OpenMPJobDefinition', { + type: 'container', + containerProperties: { + image: `${ecrRepository.repositoryUri}:latest`, + vcpus: 2, + memory: 4096, + jobRoleArn: jobRole.roleArn, + executionRoleArn: jobRole.roleArn, + // Map AWS Batch parameters to command line arguments + command: [ + '/usr/local/bin/openmp_benchmark', + '--size', 'Ref::size', + '--threads', 'Ref::threads', + '--json', 'Ref::json', + '--benchmark-type', 'Ref::benchmark-type', + '--matrix-size', 'Ref::matrix-size' + ], + environment: [ + { name: 'AWS_DEFAULT_REGION', value: this.region } + // OMP_NUM_THREADS now auto-detected at runtime for optimal CPU utilization + ], + logConfiguration: { + logDriver: 'awslogs', + options: { + 'awslogs-group': logGroup.logGroupName, + 'awslogs-stream-prefix': 'openmp' + } + } + }, + // Define parameters that can be passed from submit-job (30-min runtime, 4GB memory) + parameters: { + size: '600000000', + threads: '0', + json: 'true', + 'benchmark-type': 'simple', + 'matrix-size': '1200' + } + }); + + // Lambda function for job submission and monitoring + const jobSubmitterFunction = new lambda.Function(this, 'JobSubmitterFunction', { + runtime: lambda.Runtime.PYTHON_3_11, + handler: 'index.handler', + code: lambda.Code.fromInline(` +import json +import boto3 +import uuid +from datetime import datetime + +batch_client = boto3.client('batch') + +def handler(event, context): + try: + # Parse input parameters + problem_size = event.get('problemSize', 100000000) + max_threads = event.get('maxThreads', 0) + instance_type = event.get('instanceType', 't3.large') + + # Map instance types to optimal vCPU counts for OpenMP + instance_vcpu_map = { + 't3.medium': 2, + 't3.large': 2, + 't3.xlarge': 4, + 't3a.medium': 2, + 't3a.large': 2, + 'c6i.large': 2, + 'c6i.xlarge': 4, + 'c6i.2xlarge': 8, + 'c6i.4xlarge': 16, + 'c6i.8xlarge': 32, + 'c5.large': 2, + 'c5.xlarge': 4, + 'c5.2xlarge': 8, + 'c5.4xlarge': 16, + 'c5.9xlarge': 36, + 'c5n.large': 2, + 'c5n.xlarge': 4, + 'c5n.2xlarge': 8 + } + + # Determine optimal thread count + instance_vcpus = instance_vcpu_map.get(instance_type, 4) + optimal_threads = max_threads if max_threads > 0 else instance_vcpus + + # Generate unique job name + job_name = f"openmp-benchmark-{uuid.uuid4().hex[:8]}" + + # Submit batch job with dynamic resource allocation + response = batch_client.submit_job( + jobName=job_name, + jobQueue='${jobQueue.ref}', + jobDefinition='${jobDefinition.ref}', + parameters={ + 'size': str(problem_size), + 'threads': str(optimal_threads), + 'json': 'true' + }, + containerOverrides={ + 'vcpus': instance_vcpus, + 'memory': instance_vcpus * 2048, # 2GB per vCPU + 'environment': [ + {'name': 'AWS_BATCH_JOB_INSTANCE_TYPE', 'value': instance_type}, + {'name': 'OMP_NUM_THREADS', 'value': str(optimal_threads)} + ] + } + ) + + return { + 'statusCode': 200, + 'body': json.dumps({ + 'jobId': response['jobId'], + 'jobName': response['jobName'], + 'message': 'Job submitted successfully' + }) + } + + except Exception as e: + return { + 'statusCode': 500, + 'body': json.dumps({ + 'error': str(e) + }) + } + `), + timeout: cdk.Duration.minutes(5), + environment: { + 'JOB_QUEUE': jobQueue.ref, + 'JOB_DEFINITION': jobDefinition.ref + } + }); + + // Grant Lambda permissions to submit Batch jobs + jobSubmitterFunction.addToRolePolicy(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 'batch:SubmitJob', + 'batch:DescribeJobs', + 'batch:ListJobs' + ], + resources: ['*'] + })); + + + + // Outputs + new cdk.CfnOutput(this, 'ECRRepositoryURI', { + value: ecrRepository.repositoryUri, + description: 'ECR Repository URI for OpenMP container' + }); + + new cdk.CfnOutput(this, 'JobQueueName', { + value: jobQueue.ref, + description: 'Batch Job Queue Name' + }); + + new cdk.CfnOutput(this, 'JobDefinitionName', { + value: jobDefinition.ref, + description: 'Batch Job Definition Name' + }); + + + + new cdk.CfnOutput(this, 'LambdaFunctionName', { + value: jobSubmitterFunction.functionName, + description: 'Lambda function for job submission' + }); + } +} diff --git a/typescript/batch-ecr-openmp/package.json b/typescript/batch-ecr-openmp/package.json new file mode 100644 index 000000000..aeb9e96af --- /dev/null +++ b/typescript/batch-ecr-openmp/package.json @@ -0,0 +1,28 @@ +{ + "name": "batch-ecr-openmp", + "version": "1.0.0", + "description": "AWS Batch with containerized application in ECR and Lambda for job submission", + "private": true, + "bin": { + "batch-ecr-lambda": "bin/aws-batch-openmp-benchmark.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk" + }, + "license": "Apache-2.0", + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "22.7.9", + "aws-cdk": "2.1018.1", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "typescript": "~5.6.3" + }, + "dependencies": { + "aws-cdk-lib": "^2.200.1", + "constructs": "^10.4.2" + } +} diff --git a/typescript/batch-ecr-openmp/scripts/build-and-deploy.sh b/typescript/batch-ecr-openmp/scripts/build-and-deploy.sh new file mode 100644 index 000000000..1331c1923 --- /dev/null +++ b/typescript/batch-ecr-openmp/scripts/build-and-deploy.sh @@ -0,0 +1,117 @@ +#!/bin/bash + +# AWS Batch OpenMP Benchmark - Build and Deploy Script +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# AWS Profile (required argument) +if [ -z "$AWS_PROFILE" ]; then + echo -e "${RED}❌ AWS_PROFILE environment variable is required. Please set it before running the script.${NC}" + echo -e "${YELLOW}Example: AWS_PROFILE=my-profile ./scripts/build-and-deploy.sh${NC}" + exit 1 +fi + +echo -e "${GREEN}🚀 AWS Batch OpenMP Benchmark - Build and Deploy${NC}" +echo "==============================================" +echo -e "${YELLOW}Using AWS Profile: ${AWS_PROFILE}${NC}" +echo "" + +# Check if AWS CLI is configured with the profile +if ! aws sts get-caller-identity --profile $AWS_PROFILE > /dev/null 2>&1; then + echo -e "${RED}❌ AWS CLI not configured for profile '$AWS_PROFILE'. Please run 'aws configure --profile $AWS_PROFILE' first.${NC}" + exit 1 +fi + +# Get AWS account and region +AWS_ACCOUNT=$(aws sts get-caller-identity --profile $AWS_PROFILE --query Account --output text) +AWS_REGION=$(aws configure get region --profile $AWS_PROFILE) +ECR_REPOSITORY_URI="${AWS_ACCOUNT}.dkr.ecr.${AWS_REGION}.amazonaws.com/openmp-benchmark" + +echo -e "${YELLOW}📋 Configuration:${NC}" +echo " AWS Profile: $AWS_PROFILE" +echo " AWS Account: $AWS_ACCOUNT" +echo " AWS Region: $AWS_REGION" +echo " ECR Repository: $ECR_REPOSITORY_URI" +echo "" + +# Step 1: Build the CDK stack +echo -e "${YELLOW}🏗️ Step 1: Building CDK stack...${NC}" +npm run build +if [ $? -ne 0 ]; then + echo -e "${RED}❌ CDK build failed${NC}" + exit 1 +fi + +# Step 2: Deploy CDK stack +echo -e "${YELLOW}☁️ Step 2: Deploying CDK stack...${NC}" +npx cdk deploy --profile $AWS_PROFILE --require-approval never +if [ $? -ne 0 ]; then + echo -e "${RED}❌ CDK deployment failed${NC}" + exit 1 +fi + +# Step 3: Get ECR login token and login +echo -e "${YELLOW}🔐 Step 3: Logging into ECR...${NC}" +aws ecr get-login-password --profile $AWS_PROFILE --region $AWS_REGION | docker login --username AWS --password-stdin $ECR_REPOSITORY_URI +if [ $? -ne 0 ]; then + echo -e "${RED}❌ ECR login failed${NC}" + exit 1 +fi + +# Step 4: Build Docker image +echo -e "${YELLOW}🐳 Step 4: Building Docker image...${NC}" +docker build -f docker/Dockerfile -t openmp-benchmark:latest . +if [ $? -ne 0 ]; then + echo -e "${RED}❌ Docker build failed${NC}" + exit 1 +fi + +# Step 5: Tag and push Docker image +echo -e "${YELLOW}📤 Step 5: Pushing Docker image to ECR...${NC}" +docker tag openmp-benchmark:latest $ECR_REPOSITORY_URI:latest +docker push $ECR_REPOSITORY_URI:latest +if [ $? -ne 0 ]; then + echo -e "${RED}❌ Docker push failed${NC}" + exit 1 +fi + +# Step 6: Get stack outputs +echo -e "${YELLOW}📊 Step 6: Getting deployment information...${NC}" +STACK_OUTPUTS=$(aws cloudformation describe-stacks --profile $AWS_PROFILE --stack-name AwsBatchOpenmpBenchmarkStack --query 'Stacks[0].Outputs' --output json) + +# Step 7: Save deployment info for automated job submission +echo -e "${YELLOW}💾 Step 7: Saving deployment information...${NC}" +cat > deployment-info.json << EOF +{ + "awsProfile": "$AWS_PROFILE", + "awsRegion": "$AWS_REGION", + "awsAccount": "$AWS_ACCOUNT", + "stackOutputs": $STACK_OUTPUTS, + "deploymentTimestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" +} +EOF + +echo -e "${GREEN}✅ Deployment completed successfully!${NC}" +echo -e "${GREEN}✅ Deployment info saved to deployment-info.json${NC}" +echo "" +echo -e "${YELLOW}📋 Deployment Information:${NC}" +echo "$STACK_OUTPUTS" | jq -r '.[] | " \(.OutputKey): \(.OutputValue)"' + +echo "" +echo -e "${GREEN}🎉 Your AWS Batch OpenMP Benchmark is ready!${NC}" +echo "" +echo -e "${YELLOW}Next steps:${NC}" +echo "1. Test the Lambda function to submit jobs" +echo "2. Monitor jobs in AWS Batch console" + +echo "3. View logs in CloudWatch" +echo "" +echo -e "${YELLOW}💡 Usage Tips:${NC}" +echo "• To use a different profile: AWS_PROFILE=my-profile ./scripts/build-and-deploy.sh" +echo "• To test locally first: ./scripts/test-local.sh" +echo "• To clean up: npx cdk destroy --profile $AWS_PROFILE" diff --git a/typescript/batch-ecr-openmp/scripts/submit-job.sh b/typescript/batch-ecr-openmp/scripts/submit-job.sh new file mode 100644 index 000000000..0ccd2ee84 --- /dev/null +++ b/typescript/batch-ecr-openmp/scripts/submit-job.sh @@ -0,0 +1,319 @@ +#!/bin/bash + +# AWS Batch OpenMP Benchmark - Enhanced Job Submission Script +# Supports multiple benchmark types with optimized parameters +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +NC='\033[0m' # No Color + +# Enhanced default parameters for quick benchmarks (4GB memory limit) +DEFAULT_SIZE=600000000 # 600M elements for ~5 second runtime (4GB memory) +DEFAULT_MATRIX_SIZE=1200 # 1200x1200 matrix for ~3 second target +DEFAULT_THREADS=0 # 0 = auto-detect all available cores +DEFAULT_BENCHMARK="simple" # Default benchmark type +DEFAULT_INSTANCE="c5.large" # Cost-optimized default for demo +DEFAULT_METHOD="batch" +DRY_RUN=false + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + --size) + PROBLEM_SIZE="$2" + shift 2 + ;; + --matrix-size) + MATRIX_SIZE="$2" + shift 2 + ;; + --threads) + THREAD_COUNT="$2" + shift 2 + ;; + --benchmark-type) + BENCHMARK_TYPE="$2" + shift 2 + ;; + --instance) + INSTANCE_TYPE="$2" + shift 2 + ;; + --method) + METHOD="$2" + shift 2 + ;; + --dry-run) + DRY_RUN=true + shift + ;; + --help|-h) + echo -e "${GREEN}🚀 AWS Batch OpenMP Enhanced Benchmark Suite${NC}" + echo "=============================================" + echo "" + echo -e "${YELLOW}Usage:${NC}" + echo " $0 [OPTIONS]" + echo "" + echo -e "${YELLOW}Options:${NC}" + echo " --size SIZE Problem size for simple/math/heavy benchmarks (default: $DEFAULT_SIZE)" + echo " --matrix-size SIZE Matrix size for matrix benchmark (default: $DEFAULT_MATRIX_SIZE)" + echo " --threads COUNT Thread count, 0=auto-detect all cores (default: $DEFAULT_THREADS)" + echo " --benchmark-type TYPE Benchmark type (default: $DEFAULT_BENCHMARK)" + echo " --instance TYPE Instance type (default: $DEFAULT_INSTANCE)" + echo " --method METHOD Submission method: batch|lambda (default: $DEFAULT_METHOD)" + echo " --dry-run Show commands without executing" + echo " --help, -h Show this help message" + echo "" + echo -e "${YELLOW}Benchmark Types:${NC}" + echo " ${PURPLE}simple${NC} - Basic arithmetic operations (fastest, ~2 seconds)" + echo " ${PURPLE}math${NC} - Mathematical functions: sin, cos, sqrt, pow (~5 seconds)" + echo " ${PURPLE}matrix${NC} - Matrix multiplication (memory intensive, ~3 seconds)" + echo " ${PURPLE}heavy${NC} - Complex arithmetic expressions (~3 seconds)" + echo " ${PURPLE}all${NC} - Run all benchmarks sequentially (~15 seconds total)" + echo "" + echo -e "${YELLOW}Instance Types (Recommended):${NC}" + echo " ${BLUE}c6i.large${NC} - 2 vCPUs, 4 GB RAM (development/testing)" + echo " ${BLUE}c6i.xlarge${NC} - 4 vCPUs, 8 GB RAM (recommended default)" + echo " ${BLUE}c6i.2xlarge${NC} - 8 vCPUs, 16 GB RAM (maximum performance)" + echo "" + echo -e "${YELLOW}Examples:${NC}" + echo " $0 # Simple benchmark with defaults" + echo " $0 --benchmark-type all # Run comprehensive 5-minute suite" + echo " $0 --benchmark-type matrix --instance c6i.2xlarge # Matrix benchmark on 8 cores" + echo " $0 --benchmark-type math --size 100000000 # Math benchmark with custom size" + echo " $0 --method lambda --benchmark-type heavy # Heavy benchmark via Lambda" + echo " $0 --dry-run --benchmark-type all # Preview all benchmark commands" + echo "" + echo -e "${YELLOW}Expected Runtimes:${NC}" + echo " Simple: ~2 seconds (basic arithmetic)" + echo " Math: ~5 seconds (floating point intensive)" + echo " Matrix: ~3 seconds (memory bound)" + echo " Heavy: ~3 seconds (complex expressions)" + echo " All: ~15 seconds total (runs all benchmarks)" + exit 0 + ;; + *) + echo -e "${RED}❌ Unknown option: $1${NC}" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +# Set defaults for unspecified parameters +PROBLEM_SIZE=${PROBLEM_SIZE:-$DEFAULT_SIZE} +MATRIX_SIZE=${MATRIX_SIZE:-$DEFAULT_MATRIX_SIZE} +THREAD_COUNT=${THREAD_COUNT:-$DEFAULT_THREADS} +BENCHMARK_TYPE=${BENCHMARK_TYPE:-$DEFAULT_BENCHMARK} +INSTANCE_TYPE=${INSTANCE_TYPE:-$DEFAULT_INSTANCE} +METHOD=${METHOD:-$DEFAULT_METHOD} + +# Validate benchmark type +case $BENCHMARK_TYPE in + simple|math|matrix|heavy|all) + # Valid benchmark type + ;; + *) + echo -e "${RED}❌ Invalid benchmark type: $BENCHMARK_TYPE${NC}" + echo -e "${YELLOW}Valid types: simple, math, matrix, heavy, all${NC}" + exit 1 + ;; +esac + +echo -e "${GREEN}🚀 AWS Batch OpenMP Enhanced Benchmark Suite${NC}" +echo "=============================================" + +# Check if deployment-info.json exists +if [ ! -f "deployment-info.json" ]; then + echo -e "${RED}❌ deployment-info.json not found!${NC}" + echo "" + echo -e "${YELLOW}Please run the deployment script first:${NC}" + echo " ./scripts/build-and-deploy.sh" + echo "" + echo "This will create the deployment-info.json file with your AWS resource information." + exit 1 +fi + +# Read deployment information +echo -e "${YELLOW}📋 Reading deployment information...${NC}" +if ! command -v jq &> /dev/null; then + echo -e "${RED}❌ jq is required but not installed. Please install jq first.${NC}" + echo "" + echo -e "${YELLOW}Installation:${NC}" + echo " Ubuntu/Debian: sudo apt-get install jq" + echo " macOS: brew install jq" + echo " Amazon Linux: sudo yum install jq" + exit 1 +fi + +# Extract information from deployment-info.json +AWS_PROFILE=$(jq -r '.awsProfile' deployment-info.json) +AWS_REGION=$(jq -r '.awsRegion' deployment-info.json) +AWS_ACCOUNT=$(jq -r '.awsAccount' deployment-info.json) + +# Extract stack outputs +JOB_QUEUE_NAME=$(jq -r '.stackOutputs[] | select(.OutputKey=="JobQueueName") | .OutputValue' deployment-info.json) +JOB_DEFINITION_NAME=$(jq -r '.stackOutputs[] | select(.OutputKey=="JobDefinitionName") | .OutputValue' deployment-info.json) +LAMBDA_FUNCTION_NAME=$(jq -r '.stackOutputs[] | select(.OutputKey=="LambdaFunctionName") | .OutputValue' deployment-info.json) + + +# Validate that we have the required information +if [ "$JOB_QUEUE_NAME" = "null" ] || [ "$JOB_DEFINITION_NAME" = "null" ] || [ -z "$JOB_QUEUE_NAME" ] || [ -z "$JOB_DEFINITION_NAME" ]; then + echo -e "${RED}❌ Could not extract AWS Batch information from deployment-info.json${NC}" + echo "" + echo -e "${YELLOW}The deployment may have failed or the file is corrupted.${NC}" + echo "Please redeploy using: ./scripts/build-and-deploy.sh" + exit 1 +fi + +# Display estimated runtime based on benchmark type +case $BENCHMARK_TYPE in + simple) + ESTIMATED_TIME="~2 seconds" + ;; + math) + ESTIMATED_TIME="~5 seconds" + ;; + matrix) + ESTIMATED_TIME="~3 seconds" + ;; + heavy) + ESTIMATED_TIME="~3 seconds" + ;; + all) + ESTIMATED_TIME="~15 seconds total" + ;; +esac + +echo -e "${YELLOW}Configuration:${NC}" +echo " AWS Profile: $AWS_PROFILE" +echo " AWS Region: $AWS_REGION" +echo " Job Queue: $JOB_QUEUE_NAME" +echo " Job Definition: $JOB_DEFINITION_NAME" +echo " Benchmark Type: ${PURPLE}$BENCHMARK_TYPE${NC} (estimated: $ESTIMATED_TIME)" +echo " Problem Size: $PROBLEM_SIZE" +if [ "$BENCHMARK_TYPE" = "matrix" ]; then + echo " Matrix Size: ${MATRIX_SIZE}x${MATRIX_SIZE}" +fi +echo " Thread Count: $THREAD_COUNT $([ "$THREAD_COUNT" = "0" ] && echo "(auto-detect all cores)")" +echo " Instance Type: $INSTANCE_TYPE" +echo " Method: $METHOD" +echo "" + +# Generate unique job name with benchmark type +JOB_NAME="openmp-${BENCHMARK_TYPE}-$(date +%s)" + +# Build parameters based on benchmark type +PARAMS="size=$PROBLEM_SIZE,threads=$THREAD_COUNT,json=true" +if [ "$BENCHMARK_TYPE" != "simple" ]; then + PARAMS="$PARAMS,benchmark-type=$BENCHMARK_TYPE" +fi +if [ "$BENCHMARK_TYPE" = "matrix" ]; then + PARAMS="$PARAMS,matrix-size=$MATRIX_SIZE" +fi + +# Submit job based on method +case $METHOD in + "batch") + echo -e "${YELLOW}🚀 Submitting $BENCHMARK_TYPE benchmark job via AWS Batch...${NC}" + + BATCH_COMMAND="aws batch submit-job --profile $AWS_PROFILE \\ + --job-name \"$JOB_NAME\" \\ + --job-queue \"$JOB_QUEUE_NAME\" \\ + --job-definition \"$JOB_DEFINITION_NAME\" \\ + --parameters $PARAMS" + + if [ "$DRY_RUN" = true ]; then + echo -e "${BLUE}Dry run - would execute:${NC}" + echo "$BATCH_COMMAND" + else + echo -e "${YELLOW}Executing:${NC} $BATCH_COMMAND" + RESULT=$(eval "$BATCH_COMMAND") + if [ $? -eq 0 ]; then + JOB_ID=$(echo "$RESULT" | jq -r '.jobId') + echo "" + echo -e "${GREEN}✅ Job submitted successfully!${NC}" + echo "Job Name: $JOB_NAME" + echo "Job ID: $JOB_ID" + echo "Benchmark: ${PURPLE}$BENCHMARK_TYPE${NC}" + echo "Estimated Runtime: $ESTIMATED_TIME" + else + echo -e "${RED}❌ Job submission failed${NC}" + exit 1 + fi + fi + ;; + + "lambda") + echo -e "${YELLOW}🚀 Submitting $BENCHMARK_TYPE benchmark job via Lambda function...${NC}" + + if [ "$LAMBDA_FUNCTION_NAME" = "null" ] || [ -z "$LAMBDA_FUNCTION_NAME" ]; then + echo -e "${RED}❌ Lambda function name not found in deployment info${NC}" + exit 1 + fi + + LAMBDA_PAYLOAD="{\"problemSize\": $PROBLEM_SIZE, \"maxThreads\": $THREAD_COUNT, \"instanceType\": \"$INSTANCE_TYPE\", \"benchmarkType\": \"$BENCHMARK_TYPE\"" + if [ "$BENCHMARK_TYPE" = "matrix" ]; then + LAMBDA_PAYLOAD="$LAMBDA_PAYLOAD, \"matrixSize\": $MATRIX_SIZE" + fi + LAMBDA_PAYLOAD="$LAMBDA_PAYLOAD}" + + LAMBDA_COMMAND="aws lambda invoke --profile $AWS_PROFILE \\ + --function-name \"$LAMBDA_FUNCTION_NAME\" \\ + --cli-binary-format raw-in-base64-out \\ + --payload '$LAMBDA_PAYLOAD' \\ + response.json" + + if [ "$DRY_RUN" = true ]; then + echo -e "${BLUE}Dry run - would execute:${NC}" + echo "$LAMBDA_COMMAND" + echo "cat response.json" + else + echo -e "${YELLOW}Executing:${NC} $LAMBDA_COMMAND" + eval "$LAMBDA_COMMAND" + if [ $? -eq 0 ]; then + echo "" + echo -e "${GREEN}✅ Lambda invocation successful!${NC}" + echo -e "${YELLOW}Response:${NC}" + cat response.json | jq '.' + rm -f response.json + else + echo -e "${RED}❌ Lambda invocation failed${NC}" + exit 1 + fi + fi + ;; + + *) + echo -e "${RED}❌ Unknown method: $METHOD${NC}" + echo "Supported methods: batch, lambda" + exit 1 + ;; +esac + +echo "" +echo -e "${YELLOW}📊 Monitoring & Next Steps:${NC}" +echo "1. Monitor job status:" +echo " aws batch describe-jobs --profile $AWS_PROFILE --jobs " +echo "" +echo "2. View real-time job logs (once running):" +echo " aws logs describe-log-streams --profile $AWS_PROFILE --log-group-name /aws/batch/openmp-benchmark --order-by LastEventTime --descending" +echo "" + +echo "3. AWS Console URLs:" +echo " Batch: https://$AWS_REGION.console.aws.amazon.com/batch/home#queues" +echo " CloudWatch: https://$AWS_REGION.console.aws.amazon.com/cloudwatch/home#logsV2:log-groups/log-group//aws/batch/openmp-benchmark" +echo "" +echo -e "${PURPLE}💡 Pro Tips:${NC}" +if [ "$BENCHMARK_TYPE" = "all" ]; then + echo "• The 'all' benchmark runs 4 benchmarks sequentially - expect ~15 seconds total" + echo "• Watch CPU utilization in CloudWatch - should see sustained 90%+ usage" +fi +echo "• Thread count 0 uses auto-detection for optimal CPU core utilization" +echo "• Larger instance types (c6i.2xlarge) will show better parallel scaling" +echo "• Check CloudWatch logs for detailed performance metrics and GFLOPS" diff --git a/typescript/batch-ecr-openmp/scripts/test-local.sh b/typescript/batch-ecr-openmp/scripts/test-local.sh new file mode 100644 index 000000000..96e0a888e --- /dev/null +++ b/typescript/batch-ecr-openmp/scripts/test-local.sh @@ -0,0 +1,117 @@ +#!/bin/bash + +# AWS Batch OpenMP Benchmark - Local Testing Script +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}🧪 AWS Batch OpenMP Benchmark - Local Testing${NC}" +echo "========================" + +# Check if Docker is running +if ! docker info > /dev/null 2>&1; then + echo -e "${RED}❌ Docker is not running. Please start Docker first.${NC}" + exit 1 +fi + +# Step 1: Build the OpenMP application locally +echo -e "${YELLOW}🔨 Step 1: Building OpenMP application...${NC}" +cd src/openmp +make clean && make +if [ $? -ne 0 ]; then + echo -e "${RED}❌ Local build failed${NC}" + exit 1 +fi + +# Step 2: Run Unit Tests +echo -e "${YELLOW}🧪 Step 2: Running C++ Unit Tests...${NC}" +make unit-test +if [ $? -ne 0 ]; then + echo -e "${RED}❌ Unit tests failed${NC}" + exit 1 +fi + +# Step 3: Test the application locally +echo "" +echo -e "${YELLOW}🚀 Step 3: Testing OpenMP application locally...${NC}" +echo "" +echo -e "${YELLOW}Integration Test 1: Default settings (Sequential vs Parallel Comparison)${NC}" +./openmp_benchmark + +echo "" +echo -e "${YELLOW}Integration Test 2: JSON output${NC}" +./openmp_benchmark --json + +echo "" +echo -e "${YELLOW}Integration Test 3: Sequential only${NC}" +./openmp_benchmark --sequential-only --size 10000000 + +echo "" +echo -e "${YELLOW}Integration Test 4: Parallel only${NC}" +./openmp_benchmark --parallel-only --size 10000000 + +echo "" +echo -e "${YELLOW}Integration Test 5: Custom parameters${NC}" +./openmp_benchmark --size 10000000 --threads 2 + +echo "" +echo -e "${YELLOW}Integration Test 6: Help message${NC}" +./openmp_benchmark --help + +cd ../.. + +# Step 4: Build Docker image locally +echo "" +echo -e "${YELLOW}🐳 Step 4: Building Docker image locally...${NC}" +docker build -f docker/Dockerfile -t openmp-benchmark:test . +if [ $? -ne 0 ]; then + echo -e "${RED}❌ Docker build failed${NC}" + exit 1 +fi + +# Step 5: Test Docker container +echo -e "${YELLOW}🧪 Step 5: Testing Docker container...${NC}" +echo "" +echo -e "${YELLOW}Container Test 1: Default run${NC}" +docker run --rm openmp-benchmark:test + +echo "" +echo -e "${YELLOW}Container Test 2: Custom parameters${NC}" +docker run --rm openmp-benchmark:test --size 5000000 --threads 1 + +echo "" +echo -e "${YELLOW}Container Test 3: Help${NC}" +docker run --rm openmp-benchmark:test --help + +# Step 6: CDK Unit Tests +echo "" +echo -e "${YELLOW}🧪 Step 6: Running CDK unit tests...${NC}" +npm test +if [ $? -ne 0 ]; then + echo -e "${RED}❌ CDK unit tests failed${NC}" + exit 1 +fi + +echo "" +echo -e "${GREEN}✅ All local tests passed!${NC}" +echo "" +echo -e "${YELLOW}📋 Summary:${NC}" +echo " ✓ OpenMP application builds successfully" +echo " ✓ C++ unit tests pass (8/8 tests)" +echo " ✓ Sequential and parallel implementations validated" +echo " ✓ Integration tests pass (6 test scenarios)" +echo " ✓ Docker container builds and runs correctly" +echo " ✓ CDK unit tests pass" +echo " ✓ CDK stack synthesizes without errors" +echo "" +echo -e "${GREEN}🎉 Ready for deployment!${NC}" +echo "" +echo -e "${YELLOW}Next steps:${NC}" +echo "1. Run 'AWS_PROFILE=my-profile ./scripts/build-and-deploy.sh' to deploy to AWS" +echo "2. Or run individual components:" +echo " - 'npx cdk deploy' for infrastructure only" +echo " - 'docker build' and 'docker push' for container only" diff --git a/typescript/batch-ecr-openmp/src/openmp/Makefile b/typescript/batch-ecr-openmp/src/openmp/Makefile new file mode 100644 index 000000000..4b6f193f7 --- /dev/null +++ b/typescript/batch-ecr-openmp/src/openmp/Makefile @@ -0,0 +1,46 @@ +CXX = g++ +CXXFLAGS = -std=c++20 -O3 -fopenmp -Wall -Wextra +TARGET = openmp_benchmark +TEST_TARGET = test_benchmark +SOURCE = main.cpp +TEST_SOURCE = test_benchmark.cpp + +# Default target +all: $(TARGET) + +# Build the executable +$(TARGET): $(SOURCE) + $(CXX) $(CXXFLAGS) -o $(TARGET) $(SOURCE) + +# Build the unit test executable +$(TEST_TARGET): $(TEST_SOURCE) + $(CXX) $(CXXFLAGS) -o $(TEST_TARGET) $(TEST_SOURCE) + +# Clean build artifacts +clean: + rm -f $(TARGET) $(TEST_TARGET) *.o + +# Run unit tests +unit-test: $(TEST_TARGET) + @echo "Running unit tests..." + ./$(TEST_TARGET) + +# Test with different configurations +integration-test: $(TARGET) + @echo "Running integration tests..." + @echo "Testing with default settings:" + ./$(TARGET) + @echo "\nTesting with JSON output:" + ./$(TARGET) --json + @echo "\nTesting with custom size and threads:" + ./$(TARGET) --size 50000000 --threads 4 + +# Run all tests +test: unit-test integration-test + @echo "\nAll tests completed!" + +# Install (for Docker) +install: $(TARGET) + cp $(TARGET) /usr/local/bin/ + +.PHONY: all clean test unit-test integration-test install diff --git a/typescript/batch-ecr-openmp/src/openmp/main.cpp b/typescript/batch-ecr-openmp/src/openmp/main.cpp new file mode 100644 index 000000000..fd5baa4c9 --- /dev/null +++ b/typescript/batch-ecr-openmp/src/openmp/main.cpp @@ -0,0 +1,534 @@ +#include // For std::cout +#include // For std::vector +#include // For std::string and C++20 features like starts_with +#include // For timing +#include // For OpenMP directives +#include // For atoi, getenv +#include // For JSON formatting +#include // For mathematical functions +#include // For std::fill +#include // For random number generation + +enum class BenchmarkType { + SIMPLE, + MATH, + MATRIX, + HEAVY, + ALL +}; + +struct BenchmarkResult { + long long sum; + double execution_time; + int thread_count; + int problem_size; + std::string instance_type; + std::string version_type; // "sequential" or "parallel" + std::string benchmark_name; + double flops; // Floating point operations performed +}; + +struct MultiBenchmarkResult { + std::vector sequential_results; + std::vector parallel_results; + double total_time; + int total_threads; + std::string instance_type; +}; + +struct ComparisonResult { + BenchmarkResult sequential; + BenchmarkResult parallel; + double speedup; + double efficiency; +}; + +void printJsonComparison(const ComparisonResult& comparison) { + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); + + std::cout << "{\n"; + std::cout << " \"benchmark_type\": \"sequential_vs_parallel_comparison\",\n"; + std::cout << " \"benchmark_name\": \"" << comparison.parallel.benchmark_name << "\",\n"; + std::cout << " \"problem_size\": " << comparison.sequential.problem_size << ",\n"; + std::cout << " \"sequential\": {\n"; + std::cout << " \"calculated_sum\": " << comparison.sequential.sum << ",\n"; + std::cout << " \"execution_time_seconds\": " << std::fixed << std::setprecision(6) << comparison.sequential.execution_time << ",\n"; + std::cout << " \"gflops\": " << std::fixed << std::setprecision(2) << (comparison.sequential.flops / comparison.sequential.execution_time / 1000000000.0) << ",\n"; + std::cout << " \"thread_count\": " << comparison.sequential.thread_count << "\n"; + std::cout << " },\n"; + std::cout << " \"parallel\": {\n"; + std::cout << " \"calculated_sum\": " << comparison.parallel.sum << ",\n"; + std::cout << " \"execution_time_seconds\": " << std::fixed << std::setprecision(6) << comparison.parallel.execution_time << ",\n"; + std::cout << " \"gflops\": " << std::fixed << std::setprecision(2) << (comparison.parallel.flops / comparison.parallel.execution_time / 1000000000.0) << ",\n"; + std::cout << " \"thread_count\": " << comparison.parallel.thread_count << "\n"; + std::cout << " },\n"; + std::cout << " \"comparison\": {\n"; + std::cout << " \"speedup\": " << std::fixed << std::setprecision(2) << comparison.speedup << ",\n"; + std::cout << " \"efficiency_percent\": " << std::fixed << std::setprecision(2) << comparison.efficiency << ",\n"; + std::cout << " \"results_match\": " << (comparison.sequential.sum == comparison.parallel.sum ? "true" : "false") << "\n"; + std::cout << " },\n"; + std::cout << " \"performance_unit\": \"gflops\",\n"; + std::cout << " \"instance_type\": \"" << comparison.sequential.instance_type << "\",\n"; + std::cout << " \"timestamp\": \"" << std::put_time(std::gmtime(&time_t), "%Y-%m-%dT%H:%M:%SZ") << "\",\n"; +#ifdef _OPENMP + std::cout << " \"openmp_version\": \"" << _OPENMP << "\",\n"; +#else + std::cout << " \"openmp_version\": \"not_available\",\n"; +#endif + std::cout << " \"cpp_standard\": \"C++20\"\n"; + std::cout << "}\n"; +} + +// Simple arithmetic benchmark +BenchmarkResult runSimpleBenchmark(bool parallel, int problem_size, const std::string& instance_type) { + std::vector data(problem_size); + long long sum_result = 0; + + auto start_time = std::chrono::high_resolution_clock::now(); + + if (parallel) { + #pragma omp parallel for reduction(+:sum_result) + for (int i = 0; i < problem_size; ++i) { + data[i] = i * 2; // Simple computation + sum_result += data[i]; + } + } else { + for (int i = 0; i < problem_size; ++i) { + data[i] = i * 2; // Simple computation + sum_result += data[i]; + } + } + + auto end_time = std::chrono::high_resolution_clock::now(); + std::chrono::duration duration = end_time - start_time; + + BenchmarkResult result; + result.sum = sum_result; + result.execution_time = duration.count(); + result.thread_count = parallel ? omp_get_max_threads() : 1; + result.problem_size = problem_size; + result.instance_type = instance_type; + result.version_type = parallel ? "parallel" : "sequential"; + result.benchmark_name = "simple"; + result.flops = problem_size * 2.0; // 2 ops per element: multiply + add + + return result; +} + +// Math Functions Benchmark - CPU intensive floating point operations +BenchmarkResult runMathBenchmark(bool parallel, int problem_size, const std::string& instance_type) { + std::vector data(problem_size); + double sum_result = 0.0; + + auto start_time = std::chrono::high_resolution_clock::now(); + + if (parallel) { + #pragma omp parallel for reduction(+:sum_result) + for (int i = 0; i < problem_size; ++i) { + double x = i * 0.001; // Scale to avoid overflow + // CPU-intensive mathematical operations + double result = std::sin(x) + std::cos(x * 1.5) + std::sqrt(i + 1.0) + std::pow(x, 2.3); + data[i] = result; + sum_result += result; + } + } else { + for (int i = 0; i < problem_size; ++i) { + double x = i * 0.001; + double result = std::sin(x) + std::cos(x * 1.5) + std::sqrt(i + 1.0) + std::pow(x, 2.3); + data[i] = result; + sum_result += result; + } + } + + auto end_time = std::chrono::high_resolution_clock::now(); + std::chrono::duration duration = end_time - start_time; + + BenchmarkResult result; + result.sum = static_cast(sum_result * 1000); // Scale for integer representation + result.execution_time = duration.count(); + result.thread_count = parallel ? omp_get_max_threads() : 1; + result.problem_size = problem_size; + result.instance_type = instance_type; + result.version_type = parallel ? "parallel" : "sequential"; + result.benchmark_name = "math"; + result.flops = problem_size * 8.0; // sin + cos + sqrt + pow + 3 additions + 1 multiply + + return result; +} + +// Matrix Multiplication Benchmark - Memory intensive operations +BenchmarkResult runMatrixBenchmark(bool parallel, int matrix_size, const std::string& instance_type) { + std::vector> A(matrix_size, std::vector(matrix_size)); + std::vector> B(matrix_size, std::vector(matrix_size)); + std::vector> C(matrix_size, std::vector(matrix_size, 0.0)); + + // Initialize matrices with simple values + for (int i = 0; i < matrix_size; ++i) { + for (int j = 0; j < matrix_size; ++j) { + A[i][j] = (i + j) * 0.1; + B[i][j] = (i - j + 1) * 0.1; + } + } + + auto start_time = std::chrono::high_resolution_clock::now(); + + if (parallel) { + #pragma omp parallel for collapse(2) + for (int i = 0; i < matrix_size; ++i) { + for (int j = 0; j < matrix_size; ++j) { + for (int k = 0; k < matrix_size; ++k) { + C[i][j] += A[i][k] * B[k][j]; + } + } + } + } else { + for (int i = 0; i < matrix_size; ++i) { + for (int j = 0; j < matrix_size; ++j) { + for (int k = 0; k < matrix_size; ++k) { + C[i][j] += A[i][k] * B[k][j]; + } + } + } + } + + auto end_time = std::chrono::high_resolution_clock::now(); + std::chrono::duration duration = end_time - start_time; + + // Calculate checksum + double sum_result = 0.0; + for (int i = 0; i < matrix_size; ++i) { + for (int j = 0; j < matrix_size; ++j) { + sum_result += C[i][j]; + } + } + + BenchmarkResult result; + result.sum = static_cast(sum_result * 100); // Scale for integer representation + result.execution_time = duration.count(); + result.thread_count = parallel ? omp_get_max_threads() : 1; + result.problem_size = matrix_size * matrix_size; // Total elements + result.instance_type = instance_type; + result.version_type = parallel ? "parallel" : "sequential"; + result.benchmark_name = "matrix"; + result.flops = static_cast(matrix_size) * matrix_size * matrix_size * 2.0; // N^3 * 2 (multiply + add) + + return result; +} + +// Heavy Arithmetic Benchmark - Complex mathematical expressions +BenchmarkResult runHeavyArithmeticBenchmark(bool parallel, int problem_size, const std::string& instance_type) { + std::vector data(problem_size); + double sum_result = 0.0; + + auto start_time = std::chrono::high_resolution_clock::now(); + + if (parallel) { + #pragma omp parallel for reduction(+:sum_result) + for (int i = 0; i < problem_size; ++i) { + double x = i + 1.0; + // Complex arithmetic expression with multiple operations + double result = (x * 3.14159) + std::sqrt(x) - std::pow(x, 0.3) + + std::log(x) + std::exp(x * 0.0001) + std::sin(x * 0.01) * std::cos(x * 0.02); + data[i] = result; + sum_result += result; + } + } else { + for (int i = 0; i < problem_size; ++i) { + double x = i + 1.0; + double result = (x * 3.14159) + std::sqrt(x) - std::pow(x, 0.3) + + std::log(x) + std::exp(x * 0.0001) + std::sin(x * 0.01) * std::cos(x * 0.02); + data[i] = result; + sum_result += result; + } + } + + auto end_time = std::chrono::high_resolution_clock::now(); + std::chrono::duration duration = end_time - start_time; + + BenchmarkResult result; + result.sum = static_cast(sum_result * 1000); // Scale for integer representation + result.execution_time = duration.count(); + result.thread_count = parallel ? omp_get_max_threads() : 1; + result.problem_size = problem_size; + result.instance_type = instance_type; + result.version_type = parallel ? "parallel" : "sequential"; + result.benchmark_name = "heavy"; + result.flops = problem_size * 12.0; // Multiple math operations per element + + return result; +} + +int main(int argc, char* argv[]) { + // Parse command line arguments + int problem_size = 600000000; // Default 600M for seconds-level runtime (4GB memory limit) + int matrix_size = 1800; // Default matrix size for seconds-level target + int max_threads = 0; // 0 means use all available (auto-detect) + bool json_output = false; + BenchmarkType benchmark_type = BenchmarkType::SIMPLE; + + for (int i = 1; i < argc; i++) { + std::string arg = argv[i]; + if (arg == "--size" && i + 1 < argc) { + problem_size = std::atoi(argv[++i]); + } else if (arg == "--matrix-size" && i + 1 < argc) { + matrix_size = std::atoi(argv[++i]); + } else if (arg == "--threads" && i + 1 < argc) { + max_threads = std::atoi(argv[++i]); + } else if (arg == "--benchmark-type" && i + 1 < argc) { + std::string type = argv[++i]; + if (type == "simple") benchmark_type = BenchmarkType::SIMPLE; + else if (type == "math") benchmark_type = BenchmarkType::MATH; + else if (type == "matrix") benchmark_type = BenchmarkType::MATRIX; + else if (type == "heavy") benchmark_type = BenchmarkType::HEAVY; + else if (type == "all") benchmark_type = BenchmarkType::ALL; + else { + std::cerr << "Invalid benchmark type: " << type << "\n"; + return 1; + } + } else if (arg == "--json") { + json_output = true; + } else if (arg == "--help") { + std::cout << "Usage: " << argv[0] << " [options]\n"; + std::cout << "Options:\n"; + std::cout << " --size N Problem size (default: 750000000)\n"; + std::cout << " --matrix-size N Matrix size for matrix benchmark (default: 1200)\n"; + std::cout << " --threads N Max threads to use (default: auto-detect all)\n"; + std::cout << " --benchmark-type TYPE Benchmark type: simple|math|matrix|heavy|all (default: simple)\n"; + std::cout << " --json Output results in JSON format\n"; + std::cout << " --help Show this help message\n"; + std::cout << "\nBenchmark Types:\n"; + std::cout << " simple - Basic arithmetic operations (fastest)\n"; + std::cout << " math - Mathematical functions (sin, cos, sqrt, pow)\n"; + std::cout << " matrix - Matrix multiplication (memory intensive)\n"; + std::cout << " heavy - Complex arithmetic expressions\n"; + std::cout << " all - Run all benchmarks sequentially (~few seconds)\n"; + return 0; + } + } + + // Auto-detect thread count if not specified + if (max_threads == 0) { + max_threads = omp_get_max_threads(); + } + + // Set thread count + omp_set_num_threads(max_threads); + + // Get instance type from environment (set by AWS Batch) or detect local system + std::string instance_type = "local"; + const char* instance_env = std::getenv("AWS_BATCH_JOB_INSTANCE_TYPE"); + if (instance_env) { + instance_type = instance_env; + } else { + const char* hostname_env = std::getenv("HOSTNAME"); + if (hostname_env) { + std::string hostname = hostname_env; + if (hostname.find(".compute.internal") != std::string::npos) { + instance_type = "aws-development"; + } else { + instance_type = "local-" + hostname; + } + } else { + instance_type = "local-development"; + } + } + + if (!json_output) { + std::cout << "C++20 OpenMP Enhanced Benchmark Suite\n"; + std::cout << "====================================\n"; + + // --- Demonstrate C++20 feature: std::string::starts_with --- + std::string greeting = "Hello, C++20!"; + if (greeting.starts_with("Hello")) { + std::cout << "✓ C++20 string::starts_with works!\n"; + } + + std::cout << "Auto-detected " << max_threads << " CPU cores for optimal threading\n"; + std::cout << "Instance type: " << instance_type << "\n\n"; + } + + // Calibrated problem sizes for seconds-level "all" benchmark target + int simple_size = problem_size; // Use provided size or default 4.5B + int math_size = 100000000; // 100M elements for math functions (4GB memory) + int heavy_size = 50000000; // 50M elements for heavy arithmetic (4GB memory) + + // Run benchmarks based on type + if (benchmark_type == BenchmarkType::ALL) { + // Run all benchmarks for comprehensive comparison (seconds-level total) + std::vector seq_results, par_results; + + auto total_start = std::chrono::high_resolution_clock::now(); + + if (!json_output) { + std::cout << "Running comprehensive benchmark suite (estimated few seconds)...\n"; + std::cout << "===========================================================\n\n"; + } + + // 1. Simple Benchmark (1-1.5 minutes) + if (!json_output) std::cout << "[1/4] Running Simple Benchmark (" << simple_size << " elements)...\n"; + seq_results.push_back(runSimpleBenchmark(false, simple_size, instance_type)); + par_results.push_back(runSimpleBenchmark(true, simple_size, instance_type)); + + // 2. Math Functions Benchmark (1.5-2 minutes) + if (!json_output) std::cout << "[2/4] Running Math Functions Benchmark (" << math_size << " elements)...\n"; + seq_results.push_back(runMathBenchmark(false, math_size, instance_type)); + par_results.push_back(runMathBenchmark(true, math_size, instance_type)); + + // 3. Matrix Multiplication Benchmark (2-2.5 minutes) + if (!json_output) std::cout << "[3/4] Running Matrix Multiplication Benchmark (" << matrix_size << "x" << matrix_size << ")...\n"; + seq_results.push_back(runMatrixBenchmark(false, matrix_size, instance_type)); + par_results.push_back(runMatrixBenchmark(true, matrix_size, instance_type)); + + // 4. Heavy Arithmetic Benchmark (1-1.5 minutes) + if (!json_output) std::cout << "[4/4] Running Heavy Arithmetic Benchmark (" << heavy_size << " elements)...\n"; + seq_results.push_back(runHeavyArithmeticBenchmark(false, heavy_size, instance_type)); + par_results.push_back(runHeavyArithmeticBenchmark(true, heavy_size, instance_type)); + + auto total_end = std::chrono::high_resolution_clock::now(); + std::chrono::duration total_duration = total_end - total_start; + + // Output comprehensive JSON results + if (json_output) { + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); + + std::cout << "{\n"; + std::cout << " \"benchmark_suite\": \"comprehensive_openmp_comparison\",\n"; + std::cout << " \"total_runtime_seconds\": " << std::fixed << std::setprecision(2) << total_duration.count() << ",\n"; + std::cout << " \"benchmarks\": {\n"; + + for (size_t i = 0; i < par_results.size(); ++i) { + const auto& seq = seq_results[i]; + const auto& par = par_results[i]; + double speedup = seq.execution_time / par.execution_time; + + std::cout << " \"" << par.benchmark_name << "\": {\n"; + std::cout << " \"sequential_time\": " << std::fixed << std::setprecision(3) << seq.execution_time << ",\n"; + std::cout << " \"parallel_time\": " << std::fixed << std::setprecision(3) << par.execution_time << ",\n"; + std::cout << " \"speedup\": " << std::fixed << std::setprecision(2) << speedup << ",\n"; + std::cout << " \"efficiency_percent\": " << std::fixed << std::setprecision(1) << (speedup / par.thread_count * 100.0) << ",\n"; + std::cout << " \"problem_size\": " << par.problem_size << ",\n"; + std::cout << " \"gflops\": " << std::fixed << std::setprecision(2) << (par.flops / par.execution_time / 1000000000.0) << "\n"; + std::cout << " }" << (i < par_results.size() - 1 ? "," : "") << "\n"; + } + + std::cout << " },\n"; + std::cout << " \"hardware_info\": {\n"; + std::cout << " \"detected_cores\": " << max_threads << ",\n"; + std::cout << " \"threads_used\": " << max_threads << ",\n"; + std::cout << " \"instance_type\": \"" << instance_type << "\"\n"; + std::cout << " },\n"; + std::cout << " \"performance_unit\": \"gflops\",\n"; + std::cout << " \"timestamp\": \"" << std::put_time(std::gmtime(&time_t), "%Y-%m-%dT%H:%M:%SZ") << "\",\n"; +#ifdef _OPENMP + std::cout << " \"openmp_version\": \"" << _OPENMP << "\",\n"; +#else + std::cout << " \"openmp_version\": \"not_available\",\n"; +#endif + std::cout << " \"cpp_standard\": \"C++20\"\n"; + std::cout << "}\n"; + } else { + // Detailed console output + std::cout << "\n" << std::string(70, '=') << "\n"; + std::cout << " COMPREHENSIVE BENCHMARK RESULTS\n"; + std::cout << std::string(70, '=') << "\n"; + std::cout << "Total execution time: " << std::fixed << std::setprecision(1) << total_duration.count() << " seconds (" << (total_duration.count()/60.0) << " minutes)\n\n"; + + for (size_t i = 0; i < par_results.size(); ++i) { + const auto& seq = seq_results[i]; + const auto& par = par_results[i]; + double speedup = seq.execution_time / par.execution_time; + double efficiency = speedup / par.thread_count * 100.0; + + std::cout << "--- " << par.benchmark_name << " Benchmark ---\n"; + std::cout << "Sequential: " << std::fixed << std::setprecision(2) << seq.execution_time << "s, "; + std::cout << "Parallel: " << std::fixed << std::setprecision(2) << par.execution_time << "s\n"; + std::cout << "Speedup: " << std::fixed << std::setprecision(2) << speedup << "x, "; + std::cout << "Efficiency: " << std::fixed << std::setprecision(1) << efficiency << "%\n"; + std::cout << "GFLOPS: " << std::fixed << std::setprecision(2) << (par.flops / par.execution_time / 1000000000.0) << "\n\n"; + } + + std::cout << "Instance: " << instance_type << ", Threads: " << max_threads << "\n"; + std::cout << std::string(70, '=') << "\n"; + } + } else { + // Run individual benchmark type + BenchmarkResult seq_result, par_result; + + switch (benchmark_type) { + case BenchmarkType::SIMPLE: + if (!json_output) std::cout << "Running Simple Benchmark...\n"; + seq_result = runSimpleBenchmark(false, simple_size, instance_type); + par_result = runSimpleBenchmark(true, simple_size, instance_type); + break; + case BenchmarkType::MATH: + if (!json_output) std::cout << "Running Math Functions Benchmark...\n"; + seq_result = runMathBenchmark(false, math_size, instance_type); + par_result = runMathBenchmark(true, math_size, instance_type); + break; + case BenchmarkType::MATRIX: + if (!json_output) std::cout << "Running Matrix Multiplication Benchmark...\n"; + seq_result = runMatrixBenchmark(false, matrix_size, instance_type); + par_result = runMatrixBenchmark(true, matrix_size, instance_type); + break; + case BenchmarkType::HEAVY: + if (!json_output) std::cout << "Running Heavy Arithmetic Benchmark...\n"; + seq_result = runHeavyArithmeticBenchmark(false, heavy_size, instance_type); + par_result = runHeavyArithmeticBenchmark(true, heavy_size, instance_type); + break; + default: + break; + } + + // Calculate comparison metrics + ComparisonResult comparison; + comparison.sequential = seq_result; + comparison.parallel = par_result; + comparison.speedup = seq_result.execution_time / par_result.execution_time; + comparison.efficiency = (comparison.speedup / par_result.thread_count) * 100.0; + + if (json_output) { + printJsonComparison(comparison); + } else { + std::cout << "\n======================================\n"; + std::cout << " PERFORMANCE COMPARISON \n"; + std::cout << "======================================\n"; + + std::cout << "\n--- Sequential Results ---\n"; + std::cout << "Execution time: " << std::fixed << std::setprecision(6) << seq_result.execution_time << " seconds\n"; + std::cout << "Problem size: " << seq_result.problem_size << " elements\n"; + std::cout << "GFLOPS: " << std::fixed << std::setprecision(2) << (seq_result.flops / seq_result.execution_time / 1000000000.0) << "\n"; + + std::cout << "\n--- Parallel Results ---\n"; + std::cout << "Execution time: " << std::fixed << std::setprecision(6) << par_result.execution_time << " seconds\n"; + std::cout << "Problem size: " << par_result.problem_size << " elements\n"; + std::cout << "Threads used: " << par_result.thread_count << "\n"; + std::cout << "GFLOPS: " << std::fixed << std::setprecision(2) << (par_result.flops / par_result.execution_time / 1000000000.0) << "\n"; + + std::cout << "\n--- Comparison Metrics ---\n"; + std::cout << "Speedup: " << std::fixed << std::setprecision(2) << comparison.speedup << "x\n"; + std::cout << "Efficiency: " << std::fixed << std::setprecision(2) << comparison.efficiency << "%\n"; + + std::cout << "\n--- Performance Analysis ---\n"; + if (comparison.speedup > 1.0) { + std::cout << "✓ Parallel version is " << std::fixed << std::setprecision(2) << comparison.speedup << "x faster\n"; + } else { + std::cout << "✗ Parallel version is slower (overhead dominates)\n"; + } + + if (comparison.efficiency > 70.0) { + std::cout << "✓ Excellent parallel efficiency (" << std::fixed << std::setprecision(1) << comparison.efficiency << "%)\n"; + } else if (comparison.efficiency > 50.0) { + std::cout << "~ Good parallel efficiency (" << std::fixed << std::setprecision(1) << comparison.efficiency << "%)\n"; + } else { + std::cout << "! Poor parallel efficiency (" << std::fixed << std::setprecision(1) << comparison.efficiency << "%)\n"; + } + + std::cout << "\nInstance: " << instance_type << ", Threads: " << max_threads << "\n"; + std::cout << "======================================\n"; + } + } + + return 0; +} diff --git a/typescript/batch-ecr-openmp/src/openmp/test_benchmark.cpp b/typescript/batch-ecr-openmp/src/openmp/test_benchmark.cpp new file mode 100644 index 000000000..191ebef02 --- /dev/null +++ b/typescript/batch-ecr-openmp/src/openmp/test_benchmark.cpp @@ -0,0 +1,438 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +// Simple test framework +class SimpleTest { +private: + static int tests_run; + static int tests_passed; + static std::string current_test_name; + +public: + static void start_test(const std::string& name) { + current_test_name = name; + tests_run++; + std::cout << "Running test: " << name << " ... "; + } + + static void assert_equal(long long expected, long long actual, const std::string& message = "") { + if (expected != actual) { + std::cout << "FAILED\n"; + std::cout << " Expected: " << expected << ", Got: " << actual; + if (!message.empty()) std::cout << " (" << message << ")"; + std::cout << std::endl; + return; + } + } + + static void assert_equal(int expected, int actual, const std::string& message = "") { + if (expected != actual) { + std::cout << "FAILED\n"; + std::cout << " Expected: " << expected << ", Got: " << actual; + if (!message.empty()) std::cout << " (" << message << ")"; + std::cout << std::endl; + return; + } + } + + static void assert_equal(const std::string& expected, const std::string& actual, const std::string& message = "") { + if (expected != actual) { + std::cout << "FAILED\n"; + std::cout << " Expected: \"" << expected << "\", Got: \"" << actual << "\""; + if (!message.empty()) std::cout << " (" << message << ")"; + std::cout << std::endl; + return; + } + } + + static void assert_true(bool condition, const std::string& message = "") { + if (!condition) { + std::cout << "FAILED\n"; + std::cout << " Expected condition to be true"; + if (!message.empty()) std::cout << " (" << message << ")"; + std::cout << std::endl; + return; + } + } + + static void assert_greater_than(double value, double threshold, const std::string& message = "") { + if (value <= threshold) { + std::cout << "FAILED\n"; + std::cout << " Expected " << value << " > " << threshold; + if (!message.empty()) std::cout << " (" << message << ")"; + std::cout << std::endl; + return; + } + } + + static void assert_less_than(double value, double threshold, const std::string& message = "") { + if (value >= threshold) { + std::cout << "FAILED\n"; + std::cout << " Expected " << value << " < " << threshold; + if (!message.empty()) std::cout << " (" << message << ")"; + std::cout << std::endl; + return; + } + } + + static void assert_near(double expected, double actual, double tolerance, const std::string& message = "") { + if (std::abs(expected - actual) > tolerance) { + std::cout << "FAILED\n"; + std::cout << " Expected " << actual << " to be near " << expected << " (tolerance: " << tolerance << ")"; + if (!message.empty()) std::cout << " (" << message << ")"; + std::cout << std::endl; + return; + } + } + + static void pass_test() { + std::cout << "PASSED\n"; + tests_passed++; + } + + static void print_summary() { + std::cout << "\n=== Test Summary ===\n"; + std::cout << "Tests run: " << tests_run << "\n"; + std::cout << "Tests passed: " << tests_passed << "\n"; + std::cout << "Tests failed: " << (tests_run - tests_passed) << "\n"; + std::cout << "Success rate: " << (tests_run > 0 ? (tests_passed * 100 / tests_run) : 0) << "%\n"; + } + + static bool all_passed() { + return tests_run == tests_passed; + } +}; + +// Static member definitions +int SimpleTest::tests_run = 0; +int SimpleTest::tests_passed = 0; +std::string SimpleTest::current_test_name = ""; + +// Include the structures we want to test +struct BenchmarkResult { + long long sum; + double execution_time; + int thread_count; + int problem_size; + std::string instance_type; + std::string version_type; +}; + +struct ComparisonResult { + BenchmarkResult sequential; + BenchmarkResult parallel; + double speedup; + double efficiency; +}; + +// Function to get instance type (same logic as main.cpp) +std::string getInstanceType() { + std::string instance_type = "local"; + const char* instance_env = std::getenv("AWS_BATCH_JOB_INSTANCE_TYPE"); + if (instance_env) { + instance_type = instance_env; + } else { + // Try to get more specific local system information + const char* hostname_env = std::getenv("HOSTNAME"); + const char* user_env = std::getenv("USER"); + + if (hostname_env && user_env) { + std::string hostname = hostname_env; + if (hostname.find(".compute.internal") != std::string::npos) { + instance_type = "aws-development"; + } else { + instance_type = "local-development"; + } + } else if (hostname_env) { + instance_type = "local-" + std::string(hostname_env); + } else { + instance_type = "local-development"; + } + } + return instance_type; +} + +// Sequential benchmark function (extracted from main.cpp) +BenchmarkResult runSequentialBenchmark(int problem_size, const std::string& instance_type) { + std::vector data(problem_size); + long long sum_sequential = 0; + + auto start_time = std::chrono::high_resolution_clock::now(); + + // Sequential version - simple for loop + for (int i = 0; i < problem_size; ++i) { + data[i] = i * 2; // Simple computation + sum_sequential += data[i]; + } + + auto end_time = std::chrono::high_resolution_clock::now(); + std::chrono::duration duration = end_time - start_time; + + BenchmarkResult result; + result.sum = sum_sequential; + result.execution_time = duration.count(); + result.thread_count = 1; + result.problem_size = problem_size; + result.instance_type = instance_type; + result.version_type = "sequential"; + + return result; +} + +// Parallel benchmark function (extracted from main.cpp) +BenchmarkResult runParallelBenchmark(int problem_size, const std::string& instance_type) { + std::vector data(problem_size); + long long sum_parallel = 0; + + auto start_time = std::chrono::high_resolution_clock::now(); + + // OpenMP parallel for directive + #pragma omp parallel for reduction(+:sum_parallel) + for (int i = 0; i < problem_size; ++i) { + data[i] = i * 2; // Simple computation + sum_parallel += data[i]; + } + + auto end_time = std::chrono::high_resolution_clock::now(); + std::chrono::duration duration = end_time - start_time; + + BenchmarkResult result; + result.sum = sum_parallel; + result.execution_time = duration.count(); + result.thread_count = omp_get_max_threads(); + result.problem_size = problem_size; + result.instance_type = instance_type; + result.version_type = "parallel"; + + return result; +} + +// Helper function to calculate expected sum +long long calculateExpectedSum(int problem_size) { + // Sum of i*2 for i from 0 to problem_size-1 + // = 2 * sum(i) from 0 to n-1 + // = 2 * (n-1)*n/2 = n*(n-1) + return static_cast(problem_size) * (problem_size - 1); +} + +// Test functions +void test_sequential_benchmark_small_size() { + SimpleTest::start_test("Sequential Benchmark Small Size"); + + std::string test_instance = "test_instance"; + int problem_size = 1000; + + BenchmarkResult result = runSequentialBenchmark(problem_size, test_instance); + + // Verify basic properties + SimpleTest::assert_equal(problem_size, result.problem_size, "Problem size mismatch"); + SimpleTest::assert_equal(1, result.thread_count, "Thread count should be 1"); + SimpleTest::assert_equal(test_instance, result.instance_type, "Instance type mismatch"); + SimpleTest::assert_equal(std::string("sequential"), result.version_type, "Version type mismatch"); + + // Verify sum calculation + long long expected_sum = calculateExpectedSum(problem_size); + SimpleTest::assert_equal(expected_sum, result.sum, "Sum calculation incorrect"); + + // Verify execution time is positive and reasonable + SimpleTest::assert_greater_than(result.execution_time, 0.0, "Execution time should be positive"); + SimpleTest::assert_less_than(result.execution_time, 1.0, "Execution time should be reasonable"); + + SimpleTest::pass_test(); +} + +void test_parallel_benchmark_small_size() { + SimpleTest::start_test("Parallel Benchmark Small Size"); + + std::string test_instance = "test_instance"; + int problem_size = 1000; + + BenchmarkResult result = runParallelBenchmark(problem_size, test_instance); + + // Verify basic properties + SimpleTest::assert_equal(problem_size, result.problem_size, "Problem size mismatch"); + SimpleTest::assert_true(result.thread_count >= 1, "Thread count should be at least 1"); + SimpleTest::assert_equal(test_instance, result.instance_type, "Instance type mismatch"); + SimpleTest::assert_equal(std::string("parallel"), result.version_type, "Version type mismatch"); + + // Verify sum calculation + long long expected_sum = calculateExpectedSum(problem_size); + SimpleTest::assert_equal(expected_sum, result.sum, "Sum calculation incorrect"); + + // Verify execution time is positive and reasonable + SimpleTest::assert_greater_than(result.execution_time, 0.0, "Execution time should be positive"); + SimpleTest::assert_less_than(result.execution_time, 1.0, "Execution time should be reasonable"); + + SimpleTest::pass_test(); +} + +void test_sequential_parallel_consistency() { + SimpleTest::start_test("Sequential Parallel Consistency"); + + std::string test_instance = "test_instance"; + int problem_size = 100000; + + BenchmarkResult sequential = runSequentialBenchmark(problem_size, test_instance); + BenchmarkResult parallel = runParallelBenchmark(problem_size, test_instance); + + // Both should produce the same sum + SimpleTest::assert_equal(sequential.sum, parallel.sum, "Results should match"); + + // Both should have the same problem size + SimpleTest::assert_equal(sequential.problem_size, parallel.problem_size, "Problem sizes should match"); + SimpleTest::assert_equal(problem_size, parallel.problem_size, "Problem size should be set correctly"); + + // Thread counts should be different + SimpleTest::assert_equal(1, sequential.thread_count, "Sequential should use 1 thread"); + SimpleTest::assert_true(parallel.thread_count >= 1, "Parallel should use at least 1 thread"); + + // Both should have positive execution times + SimpleTest::assert_greater_than(sequential.execution_time, 0.0, "Sequential execution time should be positive"); + SimpleTest::assert_greater_than(parallel.execution_time, 0.0, "Parallel execution time should be positive"); + + SimpleTest::pass_test(); +} + +void test_comparison_metrics() { + SimpleTest::start_test("Comparison Metrics"); + + std::string test_instance = "test_instance"; + int problem_size = 100000; + + BenchmarkResult sequential = runSequentialBenchmark(problem_size, test_instance); + BenchmarkResult parallel = runParallelBenchmark(problem_size, test_instance); + + // Calculate comparison metrics + ComparisonResult comparison; + comparison.sequential = sequential; + comparison.parallel = parallel; + comparison.speedup = sequential.execution_time / parallel.execution_time; + comparison.efficiency = (comparison.speedup / parallel.thread_count) * 100.0; + + // Verify speedup calculation + SimpleTest::assert_greater_than(comparison.speedup, 0.0, "Speedup should be positive"); + double expected_speedup = sequential.execution_time / parallel.execution_time; + SimpleTest::assert_near(expected_speedup, comparison.speedup, 0.001, "Speedup calculation should be correct"); + + // Verify efficiency calculation + SimpleTest::assert_greater_than(comparison.efficiency, 0.0, "Efficiency should be positive"); + SimpleTest::assert_true(comparison.efficiency <= 100.0 * parallel.thread_count, "Efficiency shouldn't exceed theoretical maximum"); + + // Verify results match + SimpleTest::assert_equal(comparison.sequential.sum, comparison.parallel.sum, "Comparison results should match"); + + SimpleTest::pass_test(); +} + +void test_edge_cases() { + SimpleTest::start_test("Edge Cases"); + + std::string test_instance = "test_instance"; + + // Test with size 1 + BenchmarkResult seq_tiny = runSequentialBenchmark(1, test_instance); + BenchmarkResult par_tiny = runParallelBenchmark(1, test_instance); + + SimpleTest::assert_equal(0LL, seq_tiny.sum, "Size 1: sequential sum should be 0"); + SimpleTest::assert_equal(0LL, par_tiny.sum, "Size 1: parallel sum should be 0"); + SimpleTest::assert_equal(seq_tiny.sum, par_tiny.sum, "Size 1: results should match"); + + // Test with size 2 + BenchmarkResult seq_small = runSequentialBenchmark(2, test_instance); + BenchmarkResult par_small = runParallelBenchmark(2, test_instance); + + SimpleTest::assert_equal(2LL, seq_small.sum, "Size 2: sequential sum should be 2"); + SimpleTest::assert_equal(2LL, par_small.sum, "Size 2: parallel sum should be 2"); + SimpleTest::assert_equal(seq_small.sum, par_small.sum, "Size 2: results should match"); + + SimpleTest::pass_test(); +} + +void test_mathematical_correctness() { + SimpleTest::start_test("Mathematical Correctness"); + + std::string test_instance = "test_instance"; + std::vector test_sizes = {10, 100, 1000, 5000}; + + for (int size : test_sizes) { + BenchmarkResult sequential = runSequentialBenchmark(size, test_instance); + BenchmarkResult parallel = runParallelBenchmark(size, test_instance); + + // Calculate expected sum manually + long long expected = calculateExpectedSum(size); + + // Both should match expected value + SimpleTest::assert_equal(expected, sequential.sum, "Sequential failed for size " + std::to_string(size)); + SimpleTest::assert_equal(expected, parallel.sum, "Parallel failed for size " + std::to_string(size)); + SimpleTest::assert_equal(sequential.sum, parallel.sum, "Results don't match for size " + std::to_string(size)); + } + + SimpleTest::pass_test(); +} + +void test_thread_count_behavior() { + SimpleTest::start_test("Thread Count Behavior"); + + std::string test_instance = "test_instance"; + int problem_size = 100000; + + BenchmarkResult sequential = runSequentialBenchmark(problem_size, test_instance); + BenchmarkResult parallel = runParallelBenchmark(problem_size, test_instance); + + // Sequential should always use 1 thread + SimpleTest::assert_equal(1, sequential.thread_count, "Sequential should use exactly 1 thread"); + + // Parallel should use at least 1 thread, likely more if available + SimpleTest::assert_true(parallel.thread_count >= 1, "Parallel should use at least 1 thread"); + + // Thread count should match OpenMP's max threads + SimpleTest::assert_equal(omp_get_max_threads(), parallel.thread_count, "Thread count should match OpenMP max threads"); + + SimpleTest::pass_test(); +} + +void test_expected_sum_calculation() { + SimpleTest::start_test("Expected Sum Calculation"); + + // Test the helper function + SimpleTest::assert_equal(0LL, calculateExpectedSum(1), "Size 1 expected sum"); + SimpleTest::assert_equal(2LL, calculateExpectedSum(2), "Size 2 expected sum"); + SimpleTest::assert_equal(6LL, calculateExpectedSum(3), "Size 3 expected sum"); + SimpleTest::assert_equal(12LL, calculateExpectedSum(4), "Size 4 expected sum"); + SimpleTest::assert_equal(20LL, calculateExpectedSum(5), "Size 5 expected sum"); + + // Verify the mathematical formula: n*(n-1) for sum of i*2 from 0 to n-1 + for (int n = 1; n <= 100; n++) { + long long expected = static_cast(n) * (n - 1); + SimpleTest::assert_equal(expected, calculateExpectedSum(n), "Formula verification for n=" + std::to_string(n)); + } + + SimpleTest::pass_test(); +} + +// Main test runner +int main() { + std::cout << "OpenMP Benchmark Unit Tests\n"; + std::cout << "============================\n\n"; + + // Run all tests + test_expected_sum_calculation(); + test_sequential_benchmark_small_size(); + test_parallel_benchmark_small_size(); + test_sequential_parallel_consistency(); + test_comparison_metrics(); + test_edge_cases(); + test_mathematical_correctness(); + test_thread_count_behavior(); + + // Print summary + SimpleTest::print_summary(); + + return SimpleTest::all_passed() ? 0 : 1; +} diff --git a/typescript/batch-ecr-openmp/test/aws-batch-openmp-benchmark.test.ts b/typescript/batch-ecr-openmp/test/aws-batch-openmp-benchmark.test.ts new file mode 100644 index 000000000..e7e67f72f --- /dev/null +++ b/typescript/batch-ecr-openmp/test/aws-batch-openmp-benchmark.test.ts @@ -0,0 +1,312 @@ +import * as cdk from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import * as AwsBatchOpenmpBenchmark from '../lib/aws-batch-openmp-benchmark-stack'; + +describe('AWS Batch OpenMP Benchmark Stack', () => { + let app: cdk.App; + let stack: AwsBatchOpenmpBenchmark.AwsBatchOpenmpBenchmarkStack; + let template: Template; + + beforeEach(() => { + app = new cdk.App(); + stack = new AwsBatchOpenmpBenchmark.AwsBatchOpenmpBenchmarkStack(app, 'TestStack', { + env: { account: '123456789012', region: 'us-west-2' } + }); + template = Template.fromStack(stack); + }); + + describe('ECR Repository', () => { + test('creates ECR repository with correct configuration', () => { + template.hasResourceProperties('AWS::ECR::Repository', { + RepositoryName: 'openmp-benchmark', + LifecyclePolicy: { + LifecyclePolicyText: JSON.stringify({ + rules: [{ + rulePriority: 1, + description: 'Keep only 10 most recent images', + selection: { + tagStatus: 'any', + countType: 'imageCountMoreThan', + countNumber: 10 + }, + action: { + type: 'expire' + } + }] + }) + } + }); + }); + }); + + + + describe('VPC Configuration', () => { + test('creates VPC with correct configuration', () => { + template.hasResourceProperties('AWS::EC2::VPC', { + CidrBlock: '10.0.0.0/16', + EnableDnsHostnames: true, + EnableDnsSupport: true + }); + }); + + test('creates public and private subnets', () => { + // Public subnets + template.hasResourceProperties('AWS::EC2::Subnet', { + MapPublicIpOnLaunch: true, + CidrBlock: '10.0.0.0/24' + }); + + // Private subnets + template.hasResourceProperties('AWS::EC2::Subnet', { + MapPublicIpOnLaunch: false, + CidrBlock: '10.0.2.0/24' + }); + }); + + test('creates NAT Gateway for private subnet connectivity', () => { + template.hasResourceProperties('AWS::EC2::NatGateway', {}); + }); + + test('creates Internet Gateway', () => { + template.hasResourceProperties('AWS::EC2::InternetGateway', {}); + }); + }); + + describe('Security Group', () => { + test('creates security group for Batch', () => { + template.hasResourceProperties('AWS::EC2::SecurityGroup', { + GroupDescription: 'Security group for OpenMP Batch compute environment', + SecurityGroupEgress: [{ + CidrIp: '0.0.0.0/0', + IpProtocol: '-1' + }] + }); + }); + }); + + describe('IAM Roles', () => { + test('creates Batch service role', () => { + template.hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [{ + Effect: 'Allow', + Principal: { Service: 'batch.amazonaws.com' }, + Action: 'sts:AssumeRole' + }] + }, + ManagedPolicyArns: [{ + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSBatchServiceRole' + ]] + }] + }); + }); + + test('creates EC2 instance role', () => { + template.hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [{ + Effect: 'Allow', + Principal: { Service: 'ec2.amazonaws.com' }, + Action: 'sts:AssumeRole' + }] + }, + ManagedPolicyArns: [{ + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role' + ]] + }] + }); + }); + + test('creates job execution role', () => { + template.hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [{ + Effect: 'Allow', + Principal: { Service: 'ecs-tasks.amazonaws.com' }, + Action: 'sts:AssumeRole' + }] + }, + ManagedPolicyArns: [{ + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' + ]] + }] + }); + }); + + + }); + + describe('AWS Batch Configuration', () => { + test('creates Batch compute environment', () => { + template.hasResourceProperties('AWS::Batch::ComputeEnvironment', { + Type: 'MANAGED', + State: 'ENABLED', + ComputeResources: { + Type: 'EC2', + MinvCpus: 0, + MaxvCpus: 256, + InstanceTypes: ['c6i.large', 'c6i.xlarge', 'c6i.2xlarge', 'c5.large', 'c5.xlarge'] + } + }); + }); + + test('creates Batch job queue', () => { + template.hasResourceProperties('AWS::Batch::JobQueue', { + State: 'ENABLED', + Priority: 1 + }); + }); + + test('creates Batch job definition', () => { + template.hasResourceProperties('AWS::Batch::JobDefinition', { + Type: 'container', + ContainerProperties: { + Vcpus: 2, + Memory: 4096 + } + }); + }); + }); + + describe('Lambda Function', () => { + test('creates Lambda function for job submission', () => { + // Filter out the custom resource Lambda functions + const lambdaFunctions = template.findResources('AWS::Lambda::Function'); + const jobSubmitterFunctions = Object.entries(lambdaFunctions).filter(([key, value]: [string, any]) => { + return key.includes('JobSubmitterFunction') || + (value.Properties?.Runtime === 'python3.11'); + }); + + expect(jobSubmitterFunctions.length).toBeGreaterThanOrEqual(1); + + // Check for the main job submitter function properties + template.hasResourceProperties('AWS::Lambda::Function', { + Runtime: 'python3.11', + Handler: 'index.handler', + Timeout: 300 + }); + }); + + test('grants Lambda permissions for Batch operations', () => { + // Check that the Lambda function has Batch permissions + const policies = template.findResources('AWS::IAM::Policy'); + const jobSubmitterPolicy = Object.values(policies).find((policy: any) => + policy.Properties?.PolicyName?.includes('JobSubmitterFunction') + ); + + expect(jobSubmitterPolicy).toBeDefined(); + const statements = (jobSubmitterPolicy as any).Properties.PolicyDocument.Statement; + + // Find the statement with Batch permissions + const batchStatement = statements.find((stmt: any) => + Array.isArray(stmt.Action) && stmt.Action.some((action: string) => action.startsWith('batch:')) + ); + + expect(batchStatement).toBeDefined(); + expect(batchStatement.Action).toEqual(expect.arrayContaining([ + 'batch:SubmitJob', + 'batch:DescribeJobs', + 'batch:ListJobs' + ])); + }); + }); + + describe('CloudWatch Logs', () => { + test('creates CloudWatch log group', () => { + template.hasResourceProperties('AWS::Logs::LogGroup', { + RetentionInDays: 7 + }); + }); + }); + + describe('Stack Outputs', () => { + test('exports important resource identifiers', () => { + const outputs = template.findOutputs('*'); + + expect(outputs).toHaveProperty('ECRRepositoryURI'); + expect(outputs).toHaveProperty('JobQueueName'); + expect(outputs).toHaveProperty('JobDefinitionName'); + expect(outputs).toHaveProperty('LambdaFunctionName'); + }); + }); + + describe('Resource Count Validation', () => { + test('creates expected number of resources', () => { + // Verify we have the main resource types + expect(Object.keys(template.findResources('AWS::ECR::Repository'))).toHaveLength(1); + expect(Object.keys(template.findResources('AWS::EC2::VPC'))).toHaveLength(1); + expect(Object.keys(template.findResources('AWS::Batch::ComputeEnvironment'))).toHaveLength(1); + expect(Object.keys(template.findResources('AWS::Batch::JobQueue'))).toHaveLength(1); + expect(Object.keys(template.findResources('AWS::Batch::JobDefinition'))).toHaveLength(1); + expect(Object.keys(template.findResources('AWS::Logs::LogGroup')).length).toBeGreaterThanOrEqual(1); + + // Check Lambda functions - should have our job submitter plus potentially custom resource handlers + const lambdaFunctions = Object.keys(template.findResources('AWS::Lambda::Function')); + expect(lambdaFunctions.length).toBeGreaterThanOrEqual(1); + + // Verify we have multiple IAM roles as expected + expect(Object.keys(template.findResources('AWS::IAM::Role')).length).toBeGreaterThanOrEqual(3); + }); + }); + + describe('Script Files', () => { + test('build-and-deploy script includes deployment info capture', () => { + const fs = require('fs'); + const path = require('path'); + + const scriptPath = path.join(__dirname, '..', 'scripts', 'build-and-deploy.sh'); + const scriptContent = fs.readFileSync(scriptPath, 'utf8'); + + // Check that the script contains deployment info capture logic + expect(scriptContent).toContain('deployment-info.json'); + expect(scriptContent).toContain('stackOutputs'); + expect(scriptContent).toContain('awsProfile'); + }); + }); + + describe('Security Best Practices', () => { + + + test('IAM roles follow least privilege principle', () => { + // Verify no roles have admin access + const policies = template.findResources('AWS::IAM::Policy'); + Object.values(policies).forEach((policy: any) => { + const statements = policy.Properties?.PolicyDocument?.Statement || []; + statements.forEach((statement: any) => { + // Check that we don't have overly broad permissions + if (Array.isArray(statement.Action)) { + expect(statement.Action).not.toContain('*:*'); + } else if (typeof statement.Action === 'string') { + expect(statement.Action).not.toBe('*:*'); + } + }); + }); + }); + + test('VPC uses private subnets for compute resources', () => { + // Verify Batch compute environment uses subnets (should be private ones) + const computeEnv = template.findResources('AWS::Batch::ComputeEnvironment'); + const computeEnvProps = Object.values(computeEnv)[0] as any; + + expect(computeEnvProps.Properties.ComputeResources.Subnets).toBeDefined(); + expect(Array.isArray(computeEnvProps.Properties.ComputeResources.Subnets)).toBe(true); + expect(computeEnvProps.Properties.ComputeResources.Subnets.length).toBeGreaterThan(0); + + // Verify these are references to private subnets (they should contain 'PrivateSubnet' in the reference) + const subnetRefs = computeEnvProps.Properties.ComputeResources.Subnets; + subnetRefs.forEach((subnetRef: any) => { + expect(subnetRef.Ref).toMatch(/.*PrivateSubnet.*/); + }); + }); + }); +}); diff --git a/typescript/batch-ecr-openmp/tsconfig.json b/typescript/batch-ecr-openmp/tsconfig.json new file mode 100644 index 000000000..28bb557fa --- /dev/null +++ b/typescript/batch-ecr-openmp/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "es2022" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +}